diff --git a/package/secubox/luci-app-haproxy/README.md b/package/secubox/luci-app-haproxy/README.md new file mode 100644 index 00000000..7ef4a350 --- /dev/null +++ b/package/secubox/luci-app-haproxy/README.md @@ -0,0 +1,449 @@ +# ⚑ HAProxy Manager - Reverse Proxy Dashboard + +Enterprise-grade reverse proxy management with automatic SSL certificates, vhost configuration, and backend health monitoring. + +## ✨ Features + +| Feature | Description | +|---------|-------------| +| 🌐 **Vhost Management** | Create and manage virtual hosts | +| πŸ”’ **ACME SSL** | Automatic Let's Encrypt certificates | +| βš–οΈ **Load Balancing** | Round-robin, least-conn, source | +| πŸ₯ **Health Checks** | Backend server monitoring | +| πŸ“Š **Statistics** | Real-time traffic dashboard | +| πŸ”§ **Config Generator** | Auto-generate HAProxy config | +| 🐳 **LXC Container** | Runs isolated in container | + +## πŸš€ Quick Start + +### Create a Vhost + +1. Go to **Services β†’ HAProxy β†’ Vhosts** +2. Click **+ Add Vhost** +3. Fill in: + - **Domain**: `app.example.com` + - **Backend**: Select or create + - **SSL**: βœ… Enable + - **ACME**: βœ… Auto-certificate +4. Click **Save & Apply** + +### Architecture + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + Internet β”‚ HAProxy Container β”‚ + β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ + β–Ό β”‚ β”‚ Frontend β”‚ β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ :80 β†’ :443 redirect β”‚ β”‚ + β”‚ Port 80 │──────►│ β”‚ :443 β†’ SSL terminate β”‚ β”‚ + β”‚Port 443 β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ + β”‚ β–Ό β”‚ + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ + β”‚ β”‚ Backends β”‚ β”‚ + β”‚ β”‚ app.example.com β†’:8080 β”‚ β”‚ + β”‚ β”‚ api.example.com β†’:3000 β”‚ β”‚ + β”‚ β”‚ blog.example.comβ†’:4000 β”‚ β”‚ + β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## πŸ“Š Dashboard + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ⚑ HAProxy 🟒 Running β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ πŸ“Š Statistics β”‚ +β”‚ β”œβ”€ 🌐 Vhosts: 5 active β”‚ +β”‚ β”œβ”€ βš™οΈ Backends: 8 configured β”‚ +β”‚ β”œβ”€ πŸ”’ Certificates: 5 valid β”‚ +β”‚ └─ πŸ“ˆ Requests: 12.5K/min β”‚ +β”‚ β”‚ +β”‚ πŸ₯ Backend Health β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Backend β”‚ Status β”‚ Server β”‚ Latency β”‚ β”‚ +β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ +β”‚ β”‚ webapp β”‚ 🟒 UP β”‚ 2/2 β”‚ 12ms β”‚ β”‚ +β”‚ β”‚ api β”‚ 🟒 UP β”‚ 1/1 β”‚ 8ms β”‚ β”‚ +β”‚ β”‚ blog β”‚ 🟑 DEG β”‚ 1/2 β”‚ 45ms β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## 🌐 Vhost Configuration + +### Create Vhost + +```bash +ubus call luci.haproxy create_vhost '{ + "domain": "app.example.com", + "backend": "webapp", + "ssl": 1, + "ssl_redirect": 1, + "acme": 1, + "enabled": 1 +}' +``` + +### Vhost Options + +| Option | Default | Description | +|--------|---------|-------------| +| `domain` | - | Domain name (required) | +| `backend` | - | Backend name to route to | +| `ssl` | 1 | Enable SSL/TLS | +| `ssl_redirect` | 1 | Redirect HTTP to HTTPS | +| `acme` | 1 | Auto-request Let's Encrypt cert | +| `enabled` | 1 | Vhost active | + +### List Vhosts + +```bash +ubus call luci.haproxy list_vhosts + +# Response: +{ + "vhosts": [{ + "id": "app_example_com", + "domain": "app.example.com", + "backend": "webapp", + "ssl": true, + "ssl_redirect": true, + "acme": true, + "enabled": true, + "cert_status": "valid", + "cert_expiry": "2025-03-15" + }] +} +``` + +## βš™οΈ Backend Configuration + +### Create Backend + +```bash +ubus call luci.haproxy create_backend '{ + "name": "webapp", + "mode": "http", + "balance": "roundrobin" +}' +``` + +### Add Server to Backend + +```bash +ubus call luci.haproxy create_server '{ + "backend": "webapp", + "name": "srv1", + "address": "192.168.255.10", + "port": 8080, + "weight": 100, + "check": 1 +}' +``` + +### Backend Modes + +| Mode | Description | +|------|-------------| +| `http` | Layer 7 HTTP proxy | +| `tcp` | Layer 4 TCP proxy | + +### Load Balancing + +| Algorithm | Description | +|-----------|-------------| +| `roundrobin` | Rotate through servers | +| `leastconn` | Least active connections | +| `source` | Sticky by client IP | +| `uri` | Sticky by URI hash | + +## πŸ”’ SSL Certificates + +### ACME Auto-Certificates + +When `acme: 1` is set: +1. HAProxy serves ACME challenge on port 80 +2. Let's Encrypt validates domain ownership +3. Certificate stored in `/srv/haproxy/certs/` +4. Auto-renewal before expiry + +### Manual Certificate + +```bash +# Upload certificate +ubus call luci.haproxy upload_certificate '{ + "domain": "app.example.com", + "cert": "", + "key": "" +}' +``` + +### Certificate Status + +```bash +ubus call luci.haproxy list_certificates + +# Response: +{ + "certificates": [{ + "domain": "app.example.com", + "status": "valid", + "issuer": "Let's Encrypt", + "expiry": "2025-03-15", + "days_left": 45 + }] +} +``` + +### Request Certificate Manually + +```bash +ubus call luci.haproxy request_certificate '{"domain":"app.example.com"}' +``` + +## πŸ“Š Statistics + +### Get Stats + +```bash +ubus call luci.haproxy get_stats + +# Response: +{ + "frontend": { + "requests": 125000, + "bytes_in": 1234567890, + "bytes_out": 9876543210, + "rate": 150 + }, + "backends": [{ + "name": "webapp", + "status": "UP", + "servers_up": 2, + "servers_total": 2, + "requests": 45000, + "response_time_avg": 12 + }] +} +``` + +### Stats Page + +Access HAProxy stats at: +``` +http://192.168.255.1:8404/stats +``` + +## πŸ”§ Configuration + +### UCI Structure + +```bash +# /etc/config/haproxy + +config haproxy 'main' + option enabled '1' + option stats_port '8404' + +config backend 'webapp' + option name 'webapp' + option mode 'http' + option balance 'roundrobin' + option enabled '1' + +config server 'webapp_srv1' + option backend 'webapp' + option name 'srv1' + option address '192.168.255.10' + option port '8080' + option weight '100' + option check '1' + option enabled '1' + +config vhost 'app_example_com' + option domain 'app.example.com' + option backend 'webapp' + option ssl '1' + option ssl_redirect '1' + option acme '1' + option enabled '1' +``` + +### Generate Config + +```bash +# Regenerate haproxy.cfg from UCI +ubus call luci.haproxy generate + +# Reload HAProxy +ubus call luci.haproxy reload +``` + +### Validate Config + +```bash +ubus call luci.haproxy validate + +# Response: +{ + "valid": true, + "message": "Configuration is valid" +} +``` + +## πŸ“‘ RPCD API + +### Service Control + +| Method | Description | +|--------|-------------| +| `status` | Get HAProxy status | +| `start` | Start HAProxy service | +| `stop` | Stop HAProxy service | +| `restart` | Restart HAProxy | +| `reload` | Reload configuration | +| `generate` | Generate config file | +| `validate` | Validate configuration | + +### Vhost Management + +| Method | Description | +|--------|-------------| +| `list_vhosts` | List all vhosts | +| `create_vhost` | Create new vhost | +| `update_vhost` | Update vhost | +| `delete_vhost` | Delete vhost | + +### Backend Management + +| Method | Description | +|--------|-------------| +| `list_backends` | List all backends | +| `create_backend` | Create backend | +| `delete_backend` | Delete backend | +| `create_server` | Add server to backend | +| `delete_server` | Remove server | + +### Certificates + +| Method | Description | +|--------|-------------| +| `list_certificates` | List all certs | +| `request_certificate` | Request ACME cert | +| `upload_certificate` | Upload manual cert | +| `delete_certificate` | Delete certificate | + +## πŸ“ File Locations + +| Path | Description | +|------|-------------| +| `/etc/config/haproxy` | UCI configuration | +| `/var/lib/lxc/haproxy/` | LXC container root | +| `/srv/haproxy/haproxy.cfg` | Generated config | +| `/srv/haproxy/certs/` | SSL certificates | +| `/srv/haproxy/acme/` | ACME challenges | +| `/usr/libexec/rpcd/luci.haproxy` | RPCD backend | +| `/usr/sbin/haproxyctl` | CLI tool | + +## πŸ› οΈ CLI Tool + +### haproxyctl Commands + +```bash +# Status +haproxyctl status + +# List vhosts +haproxyctl vhosts + +# Add vhost +haproxyctl vhost add app.example.com --backend webapp --ssl --acme + +# Remove vhost +haproxyctl vhost del app.example.com + +# List certificates +haproxyctl cert list + +# Request certificate +haproxyctl cert add app.example.com + +# Generate config +haproxyctl generate + +# Reload +haproxyctl reload + +# Validate +haproxyctl validate +``` + +## πŸ› οΈ Troubleshooting + +### HAProxy Won't Start + +```bash +# Check container +lxc-info -n haproxy + +# Start container +lxc-start -n haproxy + +# Check logs +lxc-attach -n haproxy -- cat /var/log/haproxy.log +``` + +### 503 Service Unavailable + +1. Check backend is configured: + ```bash + ubus call luci.haproxy list_backends + ``` +2. Verify server is reachable: + ```bash + curl http://192.168.255.10:8080 + ``` +3. Check HAProxy logs + +### Certificate Not Working + +1. Ensure DNS resolves to your public IP +2. Ensure ports 80/443 accessible from internet +3. Check ACME challenge: + ```bash + curl http://app.example.com/.well-known/acme-challenge/test + ``` + +### Config Validation Fails + +```bash +# Show validation errors +lxc-attach -n haproxy -- haproxy -c -f /etc/haproxy/haproxy.cfg +``` + +## πŸ”’ Security + +### Firewall Rules + +HAProxy needs ports 80/443 open from WAN: + +```bash +# Auto-created when vhost uses SSL +uci show firewall | grep HAProxy +``` + +### Rate Limiting + +Add to backend config: +``` +stick-table type ip size 100k expire 30s store http_req_rate(10s) +http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 } +``` + +## πŸ“œ License + +MIT License - Copyright (C) 2025 CyberMind.fr diff --git a/package/secubox/luci-app-hexojs/README.md b/package/secubox/luci-app-hexojs/README.md new file mode 100644 index 00000000..9a27fac5 --- /dev/null +++ b/package/secubox/luci-app-hexojs/README.md @@ -0,0 +1,405 @@ +# πŸ“° Hexo CMS - Blog Publishing Platform + +Full-featured Hexo blog management with multi-instance support, Gitea integration, HAProxy publishing, and Tor hidden services. + +## ✨ Features + +| Feature | Description | +|---------|-------------| +| πŸ“ **Post Editor** | Create, edit, publish posts with markdown | +| πŸ“ **Categories/Tags** | Organize content hierarchically | +| πŸ–ΌοΈ **Media Library** | Manage images and assets | +| 🎨 **Theme Config** | Edit Hexo theme settings | +| πŸš€ **One-Click Deploy** | Generate and deploy with single click | +| πŸ”— **HAProxy Integration** | Auto-publish to clearnet with SSL | +| πŸ§… **Tor Hidden Services** | Publish to .onion addresses | +| πŸ“¦ **Gitea Sync** | Push/pull from Git repositories | +| πŸ§™ **Publishing Profiles** | Wizard presets for common setups | +| πŸ“Š **Health Monitoring** | Pipeline status and diagnostics | + +## πŸš€ Quick Start Wizard + +### Publishing Profiles + +Choose a preset to configure your blog: + +| Profile | Icon | HAProxy | Tor | Use Case | +|---------|------|---------|-----|----------| +| 🌐 **Blog** | πŸ“° | βœ… SSL | ❌ | Public blog with custom domain | +| 🎨 **Portfolio** | πŸ–ΌοΈ | βœ… SSL | ❌ | Creative showcase | +| πŸ”’ **Privacy** | πŸ§… | ❌ | βœ… | Anonymous .onion blog | +| 🌍 **Dual** | πŸŒπŸ§… | βœ… | βœ… | Clearnet + Tor access | +| πŸ“š **Documentation** | πŸ“– | βœ… SSL | ❌ | Technical docs site | + +### Apply a Profile + +```bash +# Via LuCI: Services β†’ Hexo CMS β†’ Profiles β†’ Apply + +# Via CLI +ubus call luci.hexojs apply_profile '{ + "instance": "default", + "profile": "blog", + "domain": "blog.example.com" +}' +``` + +## πŸ“Š Dashboard + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ πŸ“° Hexo CMS 🟒 Running β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ πŸ“Š Site Stats β”‚ +β”‚ β”œβ”€ πŸ“ Posts: 134 β”‚ +β”‚ β”œβ”€ πŸ“ Categories: 12 β”‚ +β”‚ β”œβ”€ 🏷️ Tags: 45 β”‚ +β”‚ └─ πŸ–ΌοΈ Media: 89 files β”‚ +β”‚ β”‚ +β”‚ πŸ”— Endpoints β”‚ +β”‚ β”œβ”€ 🏠 Local: http://192.168.255.1:4000 β”‚ +β”‚ β”œβ”€ 🌐 Clearnet: https://blog.example.com β”‚ +β”‚ └─ πŸ§… Tor: http://abc123xyz.onion β”‚ +β”‚ β”‚ +β”‚ πŸ“ˆ Pipeline Health: 95/100 β”‚ +β”‚ β”œβ”€ βœ… Hexo Server: Running β”‚ +β”‚ β”œβ”€ βœ… HAProxy: Published β”‚ +β”‚ β”œβ”€ βœ… Certificate: Valid (45 days) β”‚ +β”‚ └─ βœ… Gitea: Synced β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## πŸ“ Content Management + +### Create a Post + +1. Go to **Services β†’ Hexo CMS β†’ Posts** +2. Click **+ New Post** +3. Fill in: + - **Title**: My First Post + - **Category**: tech/tutorials + - **Tags**: hexo, blog + - **Content**: Your markdown here +4. Click **Save Draft** or **Publish** + +### Post Front Matter + +```yaml +--- +title: My First Post +date: 2025-01-28 10:30:00 +categories: + - tech + - tutorials +tags: + - hexo + - blog +--- + +Your content here... +``` + +### List Posts via CLI + +```bash +ubus call luci.hexojs list_posts '{"instance":"default","limit":10}' +``` + +## πŸš€ Publishing Pipeline + +### Full Publish Flow + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Edit β”‚ β†’ β”‚ Generateβ”‚ β†’ β”‚ Deploy β”‚ β†’ β”‚ Live β”‚ +β”‚ Posts β”‚ β”‚ HTML β”‚ β”‚ HAProxy β”‚ β”‚ Online β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ Tor β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Commands + +```bash +# Generate static files +ubus call luci.hexojs generate '{"instance":"default"}' + +# Deploy to HAProxy (clearnet) +ubus call luci.hexojs publish_to_haproxy '{ + "instance": "default", + "domain": "blog.example.com" +}' + +# Deploy to Tor (.onion) +ubus call luci.hexojs publish_to_tor '{"instance":"default"}' + +# Full pipeline (generate + deploy all) +ubus call luci.hexojs full_publish '{ + "instance": "default", + "domain": "blog.example.com", + "tor": true +}' +``` + +## πŸ”— HAProxy Integration + +### Publish to Clearnet + +1. Go to **Hexo CMS β†’ Publishing** +2. Enter domain: `blog.example.com` +3. Check **Enable SSL** +4. Click **Publish to HAProxy** + +### What Happens + +1. βœ… Creates HAProxy backend β†’ `hexo_default` +2. βœ… Creates HAProxy server β†’ `127.0.0.1:4000` +3. βœ… Creates vhost β†’ `blog.example.com` +4. βœ… Requests ACME certificate +5. βœ… Reloads HAProxy + +### Check HAProxy Status + +```bash +ubus call luci.hexojs get_haproxy_status '{"instance":"default"}' + +# Response: +{ + "published": true, + "domain": "blog.example.com", + "ssl": true, + "cert_status": "valid", + "cert_days": 45, + "dns_status": "ok" +} +``` + +## πŸ§… Tor Hidden Services + +### Create .onion Site + +```bash +ubus call luci.hexojs publish_to_tor '{"instance":"default"}' +``` + +### Get Onion Address + +```bash +ubus call luci.hexojs get_tor_status '{"instance":"default"}' + +# Response: +{ + "enabled": true, + "onion_address": "abc123xyz...def.onion", + "virtual_port": 80, + "status": "active" +} +``` + +### Access via Tor Browser + +``` +http://abc123xyz...def.onion +``` + +## πŸ“¦ Gitea Integration + +### Setup Gitea Sync + +1. Go to **Hexo CMS β†’ Git** +2. Enter repository: `user/myblog` +3. Configure credentials (optional) +4. Click **Clone** or **Pull** + +### Webhook Auto-Deploy + +Enable automatic deployment when you push to Gitea: + +```bash +ubus call luci.hexojs setup_webhook '{ + "instance": "default", + "auto_build": true +}' +``` + +### Git Operations + +```bash +# Clone repository +ubus call luci.hexojs git_clone '{ + "instance": "default", + "url": "http://192.168.255.1:3000/user/myblog.git" +}' + +# Pull latest +ubus call luci.hexojs git_pull '{"instance":"default"}' + +# Push changes +ubus call luci.hexojs git_push '{"instance":"default"}' + +# View log +ubus call luci.hexojs git_log '{"instance":"default","limit":10}' +``` + +## πŸ“Š Health Monitoring + +### Instance Health Score + +```bash +ubus call luci.hexojs get_instance_health '{"instance":"default"}' + +# Response: +{ + "instance": "default", + "score": 95, + "status": "healthy", + "checks": { + "hexo_running": true, + "content_exists": true, + "haproxy_published": true, + "ssl_valid": true, + "dns_resolves": true, + "git_clean": true + }, + "issues": [] +} +``` + +### Health Score Breakdown + +| Check | Points | Description | +|-------|--------|-------------| +| Hexo Running | 20 | Server process active | +| Content Exists | 15 | Posts directory has content | +| HAProxy Published | 20 | Vhost configured | +| SSL Valid | 15 | Certificate not expiring | +| DNS Resolves | 15 | Domain points to server | +| Git Clean | 15 | No uncommitted changes | + +### Pipeline Status + +```bash +ubus call luci.hexojs get_pipeline_status + +# Returns status of all instances +``` + +## πŸ”§ Configuration + +### UCI Settings + +```bash +# /etc/config/hexojs + +config hexojs 'main' + option enabled '1' + option instances_root '/srv/hexojs/instances' + option content_root '/srv/hexojs/content' + +config instance 'default' + option name 'default' + option enabled '1' + option port '4000' + option theme 'landscape' + # HAProxy + option haproxy_enabled '1' + option haproxy_domain 'blog.example.com' + option haproxy_ssl '1' + # Tor + option tor_enabled '1' + option tor_onion 'abc123...onion' + # Gitea + option gitea_repo 'user/myblog' + option gitea_auto_build '1' +``` + +## πŸ“ File Locations + +| Path | Description | +|------|-------------| +| `/etc/config/hexojs` | UCI configuration | +| `/srv/hexojs/instances/` | Instance directories | +| `/srv/hexojs/content/` | Shared content (posts, media) | +| `/srv/hexojs/content/source/_posts/` | Blog posts | +| `/srv/hexojs/content/source/images/` | Media files | +| `/usr/libexec/rpcd/luci.hexojs` | RPCD backend | + +## πŸ“‘ RPCD Methods + +### Content Management + +| Method | Description | +|--------|-------------| +| `list_posts` | List all posts | +| `get_post` | Get single post content | +| `create_post` | Create new post | +| `update_post` | Update post content | +| `delete_post` | Delete a post | +| `publish_post` | Move draft to published | +| `search_posts` | Search posts by query | + +### Site Operations + +| Method | Description | +|--------|-------------| +| `generate` | Generate static HTML | +| `clean` | Clean generated files | +| `deploy` | Deploy to configured targets | +| `preview_start` | Start preview server | +| `preview_status` | Check preview server | + +### Publishing + +| Method | Description | +|--------|-------------| +| `publish_to_haproxy` | Publish to clearnet | +| `unpublish_from_haproxy` | Remove from HAProxy | +| `publish_to_tor` | Create Tor hidden service | +| `unpublish_from_tor` | Remove Tor service | +| `full_publish` | Complete pipeline | + +### Monitoring + +| Method | Description | +|--------|-------------| +| `get_instance_health` | Health score & checks | +| `get_pipeline_status` | All instances status | +| `get_instance_endpoints` | All URLs for instance | + +## πŸ› οΈ Troubleshooting + +### Hexo Server Won't Start + +```bash +# Check if port is in use +netstat -tln | grep 4000 + +# Check logs +logread | grep hexo + +# Restart manually +/etc/init.d/hexojs restart +``` + +### Posts Not Showing + +1. Check posts are in `/srv/hexojs/content/source/_posts/` +2. Verify front matter format is correct +3. Run `hexo clean && hexo generate` + +### HAProxy 503 Error + +1. Verify Hexo is running on expected port +2. Check HAProxy backend configuration +3. Test local access: `curl http://127.0.0.1:4000` + +### Git Push Fails + +1. Check credentials: `ubus call luci.hexojs git_get_credentials` +2. Verify remote URL is correct +3. Check Gitea is accessible + +## πŸ“œ License + +MIT License - Copyright (C) 2025 CyberMind.fr diff --git a/package/secubox/luci-app-hexojs/root/usr/libexec/rpcd/luci.hexojs b/package/secubox/luci-app-hexojs/root/usr/libexec/rpcd/luci.hexojs index 101cab00..ebff1fe6 100755 --- a/package/secubox/luci-app-hexojs/root/usr/libexec/rpcd/luci.hexojs +++ b/package/secubox/luci-app-hexojs/root/usr/libexec/rpcd/luci.hexojs @@ -50,8 +50,9 @@ get_status() { local draft_count=0 local page_count=0 - [ -d "$site_path/source/_posts" ] && post_count=$(ls -1 "$site_path/source/_posts/"*.md 2>/dev/null | wc -l) - [ -d "$site_path/source/_drafts" ] && draft_count=$(ls -1 "$site_path/source/_drafts/"*.md 2>/dev/null | wc -l) + # Recursive count for posts (handles subdirectory categories) + [ -d "$site_path/source/_posts" ] && post_count=$(find "$site_path/source/_posts" -type f -name "*.md" ! -name "index.md" 2>/dev/null | wc -l) + [ -d "$site_path/source/_drafts" ] && draft_count=$(find "$site_path/source/_drafts" -type f -name "*.md" ! -name "index.md" 2>/dev/null | wc -l) json_add_int "post_count" "$post_count" json_add_int "draft_count" "$draft_count" @@ -95,25 +96,35 @@ get_site_stats() { local draft_count=0 local page_count=0 local media_count=0 + local tmp_posts="/tmp/hexojs_stats_$$" - [ -d "$site_path/source/_posts" ] && post_count=$(ls -1 "$site_path/source/_posts/"*.md 2>/dev/null | wc -l) - [ -d "$site_path/source/_drafts" ] && draft_count=$(ls -1 "$site_path/source/_drafts/"*.md 2>/dev/null | wc -l) + # Recursive counts (handles subdirectory categories) + [ -d "$site_path/source/_posts" ] && post_count=$(find "$site_path/source/_posts" -type f -name "*.md" ! -name "index.md" 2>/dev/null | wc -l) + [ -d "$site_path/source/_drafts" ] && draft_count=$(find "$site_path/source/_drafts" -type f -name "*.md" ! -name "index.md" 2>/dev/null | wc -l) [ -d "$site_path/source/images" ] && media_count=$(find "$site_path/source/images" -type f 2>/dev/null | wc -l) - # Count unique categories and tags from posts + # Count unique categories and tags from posts (recursive) local categories="" local tags="" if [ -d "$site_path/source/_posts" ]; then - for f in "$site_path/source/_posts/"*.md; do + find "$site_path/source/_posts" -type f -name "*.md" ! -name "index.md" 2>/dev/null > "$tmp_posts" + while read f; do [ -f "$f" ] || continue local cat=$(grep -m1 "^categories:" "$f" 2>/dev/null | sed 's/^categories:[[:space:]]*//' | tr -d '[]' | tr ',' '\n') local tag=$(grep -m1 "^tags:" "$f" 2>/dev/null | sed 's/^tags:[[:space:]]*//' | tr -d '[]' | tr ',' '\n') + # Also use directory name as category if no category in front matter + if [ -z "$cat" ]; then + local rel_path="${f#$site_path/source/_posts/}" + local cat_dir=$(dirname "$rel_path") + [ "$cat_dir" != "." ] && cat="$cat_dir" + fi categories="$categories $cat" tags="$tags $tag" - done + done < "$tmp_posts" + rm -f "$tmp_posts" fi local cat_count=$(echo "$categories" | grep -v '^$' | sort -u | wc -l) @@ -136,23 +147,38 @@ $tag" list_posts() { local site_path=$(get_site_path) local posts_dir="$site_path/source/_posts" + local tmp_posts="/tmp/hexojs_posts_$$" json_init json_add_array "posts" if [ -d "$posts_dir" ]; then - for f in "$posts_dir"/*.md; do + # Recursively find all markdown files (handles subdirectory categories) + find "$posts_dir" -type f -name "*.md" ! -name "index.md" 2>/dev/null > "$tmp_posts" + + while read f; do [ -f "$f" ] || continue local filename=$(basename "$f") local slug="${filename%.md}" - # Parse front matter - local title=$(grep -m1 "^title:" "$f" | sed 's/^title:[[:space:]]*//' | tr -d '"' | tr -d "'") - local date=$(grep -m1 "^date:" "$f" | sed 's/^date:[[:space:]]*//') - local categories=$(grep -m1 "^categories:" "$f" | sed 's/^categories:[[:space:]]*//' | tr -d '[]') - local tags=$(grep -m1 "^tags:" "$f" | sed 's/^tags:[[:space:]]*//' | tr -d '[]') - local excerpt=$(grep -m1 "^excerpt:" "$f" | sed 's/^excerpt:[[:space:]]*//' | tr -d '"') + # Get relative path from _posts for category detection + local rel_path="${f#$posts_dir/}" + local category_dir=$(dirname "$rel_path") + [ "$category_dir" = "." ] && category_dir="" + + # Parse front matter (sanitize for JSON) + local title=$(grep -m1 "^title:" "$f" 2>/dev/null | sed 's/^title:[[:space:]]*//' | tr -d '"' | tr -d "'" | tr -d '\r') + local date=$(grep -m1 "^date:" "$f" 2>/dev/null | sed 's/^date:[[:space:]]*//' | tr -d '\r') + local categories=$(grep -m1 "^categories:" "$f" 2>/dev/null | sed 's/^categories:[[:space:]]*//' | tr -d '[]' | tr -d '\r') + local tags=$(grep -m1 "^tags:" "$f" 2>/dev/null | sed 's/^tags:[[:space:]]*//' | tr -d '[]' | tr -d '\r') + local excerpt=$(grep -m1 "^excerpt:" "$f" 2>/dev/null | sed 's/^excerpt:[[:space:]]*//' | tr -d '"' | tr -d '\r' | cut -c1-200) + + # Use directory as category if not specified in front matter + [ -z "$categories" ] && [ -n "$category_dir" ] && categories="$category_dir" + + # Truncate title if too long + title=$(echo "$title" | cut -c1-150) json_add_object json_add_string "slug" "$slug" @@ -162,8 +188,11 @@ list_posts() { json_add_string "tags" "$tags" json_add_string "excerpt" "$excerpt" json_add_string "path" "$f" + json_add_string "category_dir" "$category_dir" json_close_object - done + done < "$tmp_posts" + + rm -f "$tmp_posts" fi json_close_array @@ -174,22 +203,35 @@ get_post() { read input json_load "$input" json_get_var slug slug + json_get_var path path "" json_init - if [ -z "$slug" ]; then + if [ -z "$slug" ] && [ -z "$path" ]; then json_add_boolean "success" 0 - json_add_string "error" "Slug required" + json_add_string "error" "Slug or path required" json_dump return fi local site_path=$(get_site_path) - local post_file="$site_path/source/_posts/${slug}.md" + local post_file="" - if [ ! -f "$post_file" ]; then + # If path provided, use it directly + if [ -n "$path" ] && [ -f "$path" ]; then + post_file="$path" + else + # Search for post file (first in root, then recursively) + post_file="$site_path/source/_posts/${slug}.md" + if [ ! -f "$post_file" ]; then + # Search recursively in subdirectories + post_file=$(find "$site_path/source/_posts" -type f -name "${slug}.md" 2>/dev/null | head -1) + fi + fi + + if [ -z "$post_file" ] || [ ! -f "$post_file" ]; then json_add_boolean "success" 0 - json_add_string "error" "Post not found" + json_add_string "error" "Post not found: $slug" json_dump return fi @@ -440,12 +482,16 @@ search_posts() { local site_path=$(get_site_path) local posts_dir="$site_path/source/_posts" + local tmp_posts="/tmp/hexojs_search_$$" json_init json_add_array "posts" if [ -d "$posts_dir" ]; then - for f in "$posts_dir"/*.md; do + # Recursively find all posts + find "$posts_dir" -type f -name "*.md" ! -name "index.md" 2>/dev/null > "$tmp_posts" + + while read f; do [ -f "$f" ] || continue local match=1 @@ -455,9 +501,17 @@ search_posts() { grep -qi "$query" "$f" || match=0 fi + # Get category from front matter or directory + local file_cat=$(grep -m1 "^categories:" "$f" | sed 's/^categories:[[:space:]]*//' | tr -d '[]') + if [ -z "$file_cat" ]; then + local rel_path="${f#$posts_dir/}" + local cat_dir=$(dirname "$rel_path") + [ "$cat_dir" != "." ] && file_cat="$cat_dir" + fi + # Filter by category if [ -n "$category" ] && [ "$match" = "1" ]; then - grep -qi "categories:.*$category" "$f" || match=0 + echo "$file_cat" | grep -qi "$category" || match=0 fi # Filter by tag @@ -475,9 +529,13 @@ search_posts() { json_add_string "slug" "$slug" json_add_string "title" "${title:-$slug}" json_add_string "date" "$date" + json_add_string "path" "$f" + json_add_string "categories" "$file_cat" json_close_object fi - done + done < "$tmp_posts" + + rm -f "$tmp_posts" fi json_close_array @@ -491,28 +549,44 @@ search_posts() { list_categories() { local site_path=$(get_site_path) local posts_dir="$site_path/source/_posts" + local tmp_posts="/tmp/hexojs_cats_$$" + local tmp_cats="/tmp/hexojs_cats_list_$$" + local tmp_counts="/tmp/hexojs_cats_counts_$$" json_init json_add_array "categories" if [ -d "$posts_dir" ]; then - local cats="" - for f in "$posts_dir"/*.md; do + # Recursively find all posts + find "$posts_dir" -type f -name "*.md" ! -name "index.md" 2>/dev/null > "$tmp_posts" + : > "$tmp_cats" + + while read f; do [ -f "$f" ] || continue local cat=$(grep -m1 "^categories:" "$f" | sed 's/^categories:[[:space:]]*//' | tr -d '[]' | tr ',' '\n') - cats="$cats -$cat" - done + # Use directory as category if not in front matter + if [ -z "$cat" ]; then + local rel_path="${f#$posts_dir/}" + local cat_dir=$(dirname "$rel_path") + [ "$cat_dir" != "." ] && cat="$cat_dir" + fi + echo "$cat" >> "$tmp_cats" + done < "$tmp_posts" - # Count unique categories - echo "$cats" | grep -v '^$' | sort | uniq -c | while read count name; do + # Count unique categories and save to temp file + grep -v '^$' "$tmp_cats" 2>/dev/null | sort | uniq -c > "$tmp_counts" + + # Read counts and add to JSON (avoid piped while loop to preserve JSON context) + while read count name; do name=$(echo "$name" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') [ -n "$name" ] || continue json_add_object json_add_string "name" "$name" json_add_int "count" "$count" json_close_object - done + done < "$tmp_counts" + + rm -f "$tmp_posts" "$tmp_cats" "$tmp_counts" fi json_close_array @@ -522,28 +596,38 @@ $cat" list_tags() { local site_path=$(get_site_path) local posts_dir="$site_path/source/_posts" + local tmp_posts="/tmp/hexojs_tags_$$" + local tmp_tags="/tmp/hexojs_tags_list_$$" + local tmp_counts="/tmp/hexojs_tags_counts_$$" json_init json_add_array "tags" if [ -d "$posts_dir" ]; then - local tags="" - for f in "$posts_dir"/*.md; do + # Recursively find all posts + find "$posts_dir" -type f -name "*.md" ! -name "index.md" 2>/dev/null > "$tmp_posts" + : > "$tmp_tags" + + while read f; do [ -f "$f" ] || continue local tag=$(grep -m1 "^tags:" "$f" | sed 's/^tags:[[:space:]]*//' | tr -d '[]' | tr ',' '\n') - tags="$tags -$tag" - done + echo "$tag" >> "$tmp_tags" + done < "$tmp_posts" - # Count unique tags - echo "$tags" | grep -v '^$' | sort | uniq -c | while read count name; do + # Count unique tags and save to temp file + grep -v '^$' "$tmp_tags" 2>/dev/null | sort | uniq -c > "$tmp_counts" + + # Read counts and add to JSON (avoid piped while loop to preserve JSON context) + while read count name; do name=$(echo "$name" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') [ -n "$name" ] || continue json_add_object json_add_string "name" "$name" json_add_int "count" "$count" json_close_object - done + done < "$tmp_counts" + + rm -f "$tmp_posts" "$tmp_tags" "$tmp_counts" fi json_close_array @@ -557,12 +641,17 @@ $tag" list_media() { local site_path=$(get_site_path) local media_dir="$site_path/source/images" + local tmp_media="/tmp/hexojs_media_$$" json_init json_add_array "media" if [ -d "$media_dir" ]; then - find "$media_dir" -type f | while read f; do + # Save find results to temp file to avoid piped while loop + find "$media_dir" -type f 2>/dev/null > "$tmp_media" + + while read f; do + [ -f "$f" ] || continue local filename=$(basename "$f") local size=$(stat -c %s "$f" 2>/dev/null || echo 0) local mtime=$(stat -c %Y "$f" 2>/dev/null || echo 0) @@ -575,7 +664,9 @@ list_media() { json_add_int "size" "$size" json_add_int "mtime" "$mtime" json_close_object - done + done < "$tmp_media" + + rm -f "$tmp_media" fi json_close_array @@ -1305,6 +1396,7 @@ git_log() { json_init local site_path=$(get_site_path) + local tmp_log="/tmp/hexojs_git_log_$$" if [ ! -d "$site_path/.git" ]; then json_add_boolean "success" 0 @@ -1318,7 +1410,10 @@ git_log() { json_add_boolean "success" 1 json_add_array "commits" - git log --oneline -20 2>/dev/null | while read line; do + # Save git log to temp file to avoid piped while loop + git log --oneline -20 2>/dev/null > "$tmp_log" + + while read line; do local hash=$(echo "$line" | cut -d' ' -f1) local msg=$(echo "$line" | cut -d' ' -f2-) @@ -1326,7 +1421,9 @@ git_log() { json_add_string "hash" "$hash" json_add_string "message" "$msg" json_close_object - done + done < "$tmp_log" + + rm -f "$tmp_log" json_close_array json_dump @@ -1544,6 +1641,250 @@ gitea_save_config() { json_dump } +# ============================================ +# Workflow Status (Gitea β†’ Hexo β†’ HAProxy) +# ============================================ + +get_workflow_status() { + json_init + + local site_path=$(get_site_path) + local data_path=$(uci_get main.data_path) || data_path="$DATA_PATH" + + # ── Gitea Integration Status ── + json_add_object "gitea" + + local gitea_enabled=$(uci -q get hexojs.gitea.enabled) || gitea_enabled="0" + local gitea_url=$(uci -q get hexojs.gitea.url) || gitea_url="" + local content_repo=$(uci -q get hexojs.gitea.content_repo) || content_repo="" + local auto_sync=$(uci -q get hexojs.gitea.auto_sync) || auto_sync="0" + + json_add_boolean "enabled" "$gitea_enabled" + json_add_string "url" "$gitea_url" + json_add_string "repo" "$content_repo" + json_add_boolean "auto_sync" "$auto_sync" + + # Check git repo status + if [ -d "$site_path/.git" ]; then + cd "$site_path" + local branch=$(git branch --show-current 2>/dev/null || echo "") + local remote=$(git remote get-url origin 2>/dev/null || echo "") + local last_commit=$(git log -1 --format="%h - %s" 2>/dev/null || echo "") + local last_commit_date=$(git log -1 --format="%ci" 2>/dev/null || echo "") + local ahead=$(git rev-list --count @{u}..HEAD 2>/dev/null || echo "0") + local behind=$(git rev-list --count HEAD..@{u} 2>/dev/null || echo "0") + local modified=$(git status --porcelain 2>/dev/null | grep -c '^.M' || echo "0") + local untracked=$(git status --porcelain 2>/dev/null | grep -c '^??' || echo "0") + + json_add_boolean "has_repo" 1 + json_add_string "branch" "$branch" + json_add_string "remote" "$remote" + json_add_string "last_commit" "$last_commit" + json_add_string "last_commit_date" "$last_commit_date" + json_add_int "ahead" "$ahead" + json_add_int "behind" "$behind" + json_add_int "modified" "$modified" + json_add_int "untracked" "$untracked" + + # Sync status + if [ "$behind" -gt 0 ]; then + json_add_string "sync_status" "behind" + json_add_string "sync_message" "Pull required ($behind commits behind)" + elif [ "$ahead" -gt 0 ]; then + json_add_string "sync_status" "ahead" + json_add_string "sync_message" "Push available ($ahead commits ahead)" + elif [ "$modified" -gt 0 ] || [ "$untracked" -gt 0 ]; then + json_add_string "sync_status" "modified" + json_add_string "sync_message" "Local changes pending" + else + json_add_string "sync_status" "synced" + json_add_string "sync_message" "Up to date" + fi + else + json_add_boolean "has_repo" 0 + json_add_string "sync_status" "not_initialized" + json_add_string "sync_message" "Git repository not configured" + fi + + json_close_object + + # ── Hexo Build Status ── + json_add_object "hexo" + + local running=$(is_running && echo 1 || echo 0) + local http_port=$(uci_get main.http_port) || http_port="4000" + local post_count=0 + local draft_count=0 + + [ -d "$site_path/source/_posts" ] && post_count=$(find "$site_path/source/_posts" -type f -name "*.md" ! -name "index.md" 2>/dev/null | wc -l) + [ -d "$site_path/source/_drafts" ] && draft_count=$(find "$site_path/source/_drafts" -type f -name "*.md" ! -name "index.md" 2>/dev/null | wc -l) + + json_add_boolean "container_running" "$running" + json_add_int "http_port" "$http_port" + json_add_int "post_count" "$post_count" + json_add_int "draft_count" "$draft_count" + + # Check if public/ exists and its freshness + local public_dir="$site_path/public" + if [ -d "$public_dir" ]; then + json_add_boolean "site_built" 1 + local public_files=$(find "$public_dir" -type f 2>/dev/null | wc -l) + json_add_int "public_files" "$public_files" + + # Check build freshness + local newest_post=$(find "$site_path/source/_posts" -type f -name "*.md" -printf '%T@\n' 2>/dev/null | sort -rn | head -1) + local newest_public=$(find "$public_dir" -type f -name "*.html" -printf '%T@\n' 2>/dev/null | sort -rn | head -1) + + if [ -n "$newest_post" ] && [ -n "$newest_public" ]; then + if [ "${newest_post%.*}" -gt "${newest_public%.*}" ]; then + json_add_string "build_status" "outdated" + json_add_string "build_message" "Rebuild required (source newer than build)" + else + json_add_string "build_status" "current" + json_add_string "build_message" "Build is up to date" + fi + else + json_add_string "build_status" "unknown" + json_add_string "build_message" "Unable to determine build freshness" + fi + else + json_add_boolean "site_built" 0 + json_add_int "public_files" 0 + json_add_string "build_status" "not_built" + json_add_string "build_message" "Site has not been generated" + fi + + json_close_object + + # ── HAProxy Publishing Status ── + json_add_object "haproxy" + + local site_url=$(uci -q get hexojs.default.url) || site_url="" + local domain="" + # Extract domain from URL + if [ -n "$site_url" ]; then + domain=$(echo "$site_url" | sed 's|^https\?://||' | sed 's|/.*$||') + fi + + json_add_string "site_url" "$site_url" + json_add_string "domain" "$domain" + + # Check HAProxy vhost existence + local haproxy_running=$(pgrep haproxy >/dev/null 2>&1 && echo 1 || echo 0) + json_add_boolean "running" "$haproxy_running" + + if [ -n "$domain" ]; then + # Check if vhost exists via ubus + local vhost_exists=$(ubus call luci.haproxy list_vhosts 2>/dev/null | grep -q "\"$domain\"" && echo 1 || echo 0) + json_add_boolean "vhost_configured" "$vhost_exists" + + # Check certificate status + local cert_status="" + local cert_file="/etc/haproxy/certs/${domain}.pem" + if [ -f "$cert_file" ]; then + local expiry=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | sed 's/notAfter=//') + if [ -n "$expiry" ]; then + local expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || echo 0) + local now_epoch=$(date +%s) + local days_left=$(( (expiry_epoch - now_epoch) / 86400 )) + + if [ "$days_left" -lt 0 ]; then + cert_status="expired" + elif [ "$days_left" -lt 7 ]; then + cert_status="critical" + elif [ "$days_left" -lt 30 ]; then + cert_status="expiring" + else + cert_status="valid" + fi + json_add_int "cert_days_left" "$days_left" + fi + else + cert_status="missing" + fi + json_add_string "cert_status" "$cert_status" + + # Publishing status + if [ "$vhost_exists" = "1" ] && [ "$cert_status" = "valid" ]; then + json_add_string "publish_status" "published" + json_add_string "publish_message" "Live at https://$domain" + elif [ "$vhost_exists" = "1" ]; then + json_add_string "publish_status" "partial" + json_add_string "publish_message" "Vhost configured, certificate issue" + else + json_add_string "publish_status" "not_published" + json_add_string "publish_message" "Not published to HAProxy" + fi + else + json_add_boolean "vhost_configured" 0 + json_add_string "cert_status" "no_domain" + json_add_string "publish_status" "not_configured" + json_add_string "publish_message" "No domain configured" + fi + + json_close_object + + # ── Portal Status ── + json_add_object "portal" + + local portal_path=$(uci -q get hexojs.portal.path) || portal_path="/www/blog" + json_add_string "path" "$portal_path" + + if [ -d "$portal_path" ]; then + local portal_files=$(find "$portal_path" -type f 2>/dev/null | wc -l) + json_add_boolean "deployed" 1 + json_add_int "file_count" "$portal_files" + + # Check portal freshness vs public/ + local portal_mtime=$(stat -c %Y "$portal_path/index.html" 2>/dev/null || echo 0) + local public_mtime=$(stat -c %Y "$site_path/public/index.html" 2>/dev/null || echo 0) + + if [ "$public_mtime" -gt "$portal_mtime" ]; then + json_add_string "deploy_status" "outdated" + json_add_string "deploy_message" "Portal needs republishing" + else + json_add_string "deploy_status" "current" + json_add_string "deploy_message" "Portal is up to date" + fi + else + json_add_boolean "deployed" 0 + json_add_int "file_count" 0 + json_add_string "deploy_status" "not_deployed" + json_add_string "deploy_message" "Not deployed to portal" + fi + + json_close_object + + # ── Overall Workflow Status ── + json_add_object "workflow" + + # Calculate overall health + local issues=0 + local warnings=0 + + # Check each stage + [ ! -d "$site_path/.git" ] && issues=$((issues + 1)) + [ "$running" != "1" ] && issues=$((issues + 1)) + [ ! -d "$public_dir" ] && issues=$((issues + 1)) + + if [ "$issues" -eq 0 ]; then + json_add_string "status" "healthy" + json_add_string "message" "All workflow stages operational" + elif [ "$issues" -eq 1 ]; then + json_add_string "status" "warning" + json_add_string "message" "One workflow stage needs attention" + else + json_add_string "status" "critical" + json_add_string "message" "Multiple workflow stages need attention" + fi + + json_add_int "issues" "$issues" + + json_close_object + + json_dump +} + # Publish to /www (portal) publish_to_www() { read input @@ -1583,6 +1924,943 @@ publish_to_www() { json_dump } +# ============================================ +# Publishing Profiles (Wizard) +# ============================================ + +# List available publishing profiles +list_profiles() { + json_init + json_add_array "profiles" + + # Blog Profile + json_add_object + json_add_string "id" "blog" + json_add_string "name" "Blog" + json_add_string "description" "Personal blog with public domain and optional Tor" + json_add_string "icon" "πŸ“" + json_add_boolean "haproxy" 1 + json_add_boolean "tor" 0 + json_add_boolean "acme" 1 + json_add_int "default_port" 4000 + json_close_object + + # Portfolio Profile + json_add_object + json_add_string "id" "portfolio" + json_add_string "name" "Portfolio" + json_add_string "description" "Professional portfolio with SSL" + json_add_string "icon" "πŸ’Ό" + json_add_boolean "haproxy" 1 + json_add_boolean "tor" 0 + json_add_boolean "acme" 1 + json_add_int "default_port" 4001 + json_close_object + + # Privacy Blog Profile + json_add_object + json_add_string "id" "privacy" + json_add_string "name" "Privacy Blog" + json_add_string "description" "Tor-only hidden service blog" + json_add_string "icon" "πŸ§…" + json_add_boolean "haproxy" 0 + json_add_boolean "tor" 1 + json_add_boolean "acme" 0 + json_add_int "default_port" 4002 + json_close_object + + # Dual Access Profile + json_add_object + json_add_string "id" "dual" + json_add_string "name" "Dual Access" + json_add_string "description" "Public domain + Tor hidden service" + json_add_string "icon" "🌐" + json_add_boolean "haproxy" 1 + json_add_boolean "tor" 1 + json_add_boolean "acme" 1 + json_add_int "default_port" 4003 + json_close_object + + # Documentation Profile + json_add_object + json_add_string "id" "docs" + json_add_string "name" "Documentation" + json_add_string "description" "Internal documentation site" + json_add_string "icon" "πŸ“š" + json_add_boolean "haproxy" 1 + json_add_boolean "tor" 0 + json_add_boolean "acme" 0 + json_add_int "default_port" 4004 + json_close_object + + json_close_array + json_dump +} + +# Apply a publishing profile to an instance +apply_profile() { + read input + json_load "$input" + json_get_var instance instance + json_get_var profile profile + json_get_var domain domain + json_get_var enable_tor enable_tor + + json_init + + if [ -z "$instance" ] || [ -z "$profile" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance and profile required" + json_dump + return + fi + + local port=$(uci -q get hexojs.${instance}.port) + if [ -z "$port" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance not found: $instance" + json_dump + return + fi + + local results="" + + # Apply profile settings + case "$profile" in + blog|portfolio|dual|docs) + if [ -n "$domain" ]; then + # Create HAProxy backend and vhost + local haproxy_result=$(create_haproxy_vhost "$instance" "$domain" "$port" 1) + results="$results HAProxy:$haproxy_result" + uci set hexojs.${instance}.domain="$domain" + fi + ;; + privacy) + enable_tor="1" + ;; + esac + + # Enable Tor if requested + if [ "$enable_tor" = "1" ] || [ "$profile" = "privacy" ] || [ "$profile" = "dual" ]; then + local tor_result=$(create_tor_hidden_service "$instance" "$port") + results="$results Tor:$tor_result" + uci set hexojs.${instance}.tor_enabled="1" + fi + + uci set hexojs.${instance}.profile="$profile" + uci commit hexojs + + json_add_boolean "success" 1 + json_add_string "message" "Profile '$profile' applied to instance '$instance'" + json_add_string "results" "$results" + + json_dump +} + +# ============================================ +# HAProxy Integration +# ============================================ + +# Internal: Create HAProxy vhost for instance +create_haproxy_vhost() { + local instance="$1" + local domain="$2" + local port="$3" + local acme="$4" + + # Check if HAProxy RPCD is available + if ! ubus list | grep -q "luci.haproxy"; then + echo "haproxy_unavailable" + return 1 + fi + + # Create backend + ubus call luci.haproxy create_backend \ + "{\"name\":\"hexo_${instance}\",\"mode\":\"http\"}" 2>/dev/null + + # Create server in backend + ubus call luci.haproxy create_server \ + "{\"backend\":\"hexo_${instance}\",\"name\":\"${instance}\",\"address\":\"127.0.0.1\",\"port\":${port}}" 2>/dev/null + + # Create vhost + local vhost_params="{\"domain\":\"${domain}\",\"backend\":\"hexo_${instance}\",\"ssl\":true,\"ssl_redirect\":true" + [ "$acme" = "1" ] && vhost_params="${vhost_params},\"acme\":true" + vhost_params="${vhost_params}}" + + ubus call luci.haproxy create_vhost "$vhost_params" 2>/dev/null + + # Request certificate if ACME enabled + if [ "$acme" = "1" ]; then + ubus call luci.haproxy request_certificate "{\"domain\":\"${domain}\"}" 2>/dev/null & + fi + + echo "ok" + return 0 +} + +# Publish instance to HAProxy +publish_to_haproxy() { + read input + json_load "$input" + json_get_var instance instance + json_get_var domain domain + json_get_var acme acme + + json_init + + if [ -z "$instance" ] || [ -z "$domain" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance and domain required" + json_dump + return + fi + + local port=$(uci -q get hexojs.${instance}.port) + if [ -z "$port" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance not found: $instance" + json_dump + return + fi + + [ -z "$acme" ] && acme="1" + + local result=$(create_haproxy_vhost "$instance" "$domain" "$port" "$acme") + + if [ "$result" = "ok" ]; then + uci set hexojs.${instance}.domain="$domain" + uci set hexojs.${instance}.haproxy_enabled="1" + uci commit hexojs + + json_add_boolean "success" 1 + json_add_string "message" "Published to HAProxy" + json_add_string "domain" "$domain" + json_add_string "url" "https://${domain}" + else + json_add_boolean "success" 0 + json_add_string "error" "Failed to create HAProxy vhost: $result" + fi + + json_dump +} + +# Unpublish from HAProxy +unpublish_from_haproxy() { + read input + json_load "$input" + json_get_var instance instance + + json_init + + if [ -z "$instance" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance required" + json_dump + return + fi + + local domain=$(uci -q get hexojs.${instance}.domain) + + if [ -n "$domain" ]; then + # Delete vhost + ubus call luci.haproxy delete_vhost "{\"domain\":\"${domain}\"}" 2>/dev/null + # Delete backend + ubus call luci.haproxy delete_backend "{\"name\":\"hexo_${instance}\"}" 2>/dev/null + fi + + uci delete hexojs.${instance}.domain 2>/dev/null + uci set hexojs.${instance}.haproxy_enabled="0" + uci commit hexojs + + json_add_boolean "success" 1 + json_add_string "message" "Unpublished from HAProxy" + + json_dump +} + +# Get HAProxy status for instance +get_haproxy_status() { + read input + json_load "$input" + json_get_var instance instance + + json_init + + if [ -z "$instance" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance required" + json_dump + return + fi + + local domain=$(uci -q get hexojs.${instance}.domain) + local port=$(uci -q get hexojs.${instance}.port) + + json_add_boolean "success" 1 + json_add_string "instance" "$instance" + json_add_string "domain" "$domain" + json_add_int "port" "$port" + + if [ -n "$domain" ]; then + json_add_boolean "published" 1 + json_add_string "url" "https://${domain}" + + # Check certificate + local cert_file="/etc/haproxy/certs/${domain}.pem" + if [ -f "$cert_file" ]; then + local expiry=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | sed 's/notAfter=//') + if [ -n "$expiry" ]; then + local expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || echo 0) + local now_epoch=$(date +%s) + local days_left=$(( (expiry_epoch - now_epoch) / 86400 )) + json_add_string "cert_status" "valid" + json_add_int "cert_days_left" "$days_left" + json_add_string "cert_expiry" "$expiry" + fi + else + json_add_string "cert_status" "missing" + fi + + # Check DNS + local resolved_ip=$(nslookup "$domain" 2>/dev/null | grep -A1 "Name:" | grep "Address:" | awk '{print $2}' | head -1) + if [ -n "$resolved_ip" ]; then + json_add_string "dns_status" "ok" + json_add_string "dns_resolved_ip" "$resolved_ip" + else + json_add_string "dns_status" "failed" + fi + else + json_add_boolean "published" 0 + fi + + json_dump +} + +# ============================================ +# Tor Hidden Service Integration +# ============================================ + +# Internal: Create Tor hidden service +create_tor_hidden_service() { + local instance="$1" + local port="$2" + + # Check if Tor Shield RPCD is available + if ! ubus list | grep -q "luci.tor-shield"; then + echo "tor_unavailable" + return 1 + fi + + # Create hidden service + ubus call luci.tor-shield add_hidden_service \ + "{\"name\":\"hexo_${instance}\",\"local_port\":${port},\"virtual_port\":80}" 2>/dev/null + + # Wait for onion address (up to 10 seconds) + local onion="" + local tries=0 + while [ -z "$onion" ] && [ "$tries" -lt 10 ]; do + sleep 1 + onion=$(cat /var/lib/tor/hidden_service_hexo_${instance}/hostname 2>/dev/null) + tries=$((tries + 1)) + done + + if [ -n "$onion" ]; then + echo "$onion" + return 0 + else + echo "pending" + return 0 + fi +} + +# Publish instance to Tor +publish_to_tor() { + read input + json_load "$input" + json_get_var instance instance + + json_init + + if [ -z "$instance" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance required" + json_dump + return + fi + + local port=$(uci -q get hexojs.${instance}.port) + if [ -z "$port" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance not found: $instance" + json_dump + return + fi + + local onion=$(create_tor_hidden_service "$instance" "$port") + + if [ "$onion" != "tor_unavailable" ]; then + uci set hexojs.${instance}.tor_enabled="1" + [ "$onion" != "pending" ] && uci set hexojs.${instance}.onion_address="$onion" + uci commit hexojs + + json_add_boolean "success" 1 + json_add_string "message" "Tor hidden service created" + + if [ "$onion" != "pending" ]; then + json_add_string "onion_address" "$onion" + json_add_string "url" "http://${onion}" + else + json_add_string "status" "pending" + json_add_string "message" "Onion address generating, check back soon" + fi + else + json_add_boolean "success" 0 + json_add_string "error" "Tor Shield not available" + fi + + json_dump +} + +# Unpublish from Tor +unpublish_from_tor() { + read input + json_load "$input" + json_get_var instance instance + + json_init + + if [ -z "$instance" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance required" + json_dump + return + fi + + # Remove hidden service + ubus call luci.tor-shield remove_hidden_service "{\"name\":\"hexo_${instance}\"}" 2>/dev/null + + uci delete hexojs.${instance}.onion_address 2>/dev/null + uci set hexojs.${instance}.tor_enabled="0" + uci commit hexojs + + json_add_boolean "success" 1 + json_add_string "message" "Unpublished from Tor" + + json_dump +} + +# Get Tor status for instance +get_tor_status() { + read input + json_load "$input" + json_get_var instance instance + + json_init + + if [ -z "$instance" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance required" + json_dump + return + fi + + local tor_enabled=$(uci -q get hexojs.${instance}.tor_enabled) + local onion=$(uci -q get hexojs.${instance}.onion_address) + + # Try to get onion from file if not in UCI + if [ -z "$onion" ]; then + onion=$(cat /var/lib/tor/hidden_service_hexo_${instance}/hostname 2>/dev/null) + [ -n "$onion" ] && uci set hexojs.${instance}.onion_address="$onion" && uci commit hexojs + fi + + json_add_boolean "success" 1 + json_add_string "instance" "$instance" + json_add_boolean "enabled" "${tor_enabled:-0}" + + if [ -n "$onion" ]; then + json_add_string "onion_address" "$onion" + json_add_string "url" "http://${onion}" + json_add_string "status" "active" + elif [ "$tor_enabled" = "1" ]; then + json_add_string "status" "pending" + else + json_add_string "status" "disabled" + fi + + json_dump +} + +# ============================================ +# Full Publishing Pipeline +# ============================================ + +# Get all publishing endpoints for an instance +get_instance_endpoints() { + read input + json_load "$input" + json_get_var instance instance + + json_init + + if [ -z "$instance" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance required" + json_dump + return + fi + + local port=$(uci -q get hexojs.${instance}.port) + local domain=$(uci -q get hexojs.${instance}.domain) + local onion=$(uci -q get hexojs.${instance}.onion_address) + local lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.255.1") + + json_add_boolean "success" 1 + json_add_string "instance" "$instance" + + json_add_array "endpoints" + + # Local endpoint + json_add_object + json_add_string "type" "local" + json_add_string "url" "http://${lan_ip}:${port}" + json_add_string "status" "active" + json_add_string "icon" "🏠" + json_close_object + + # HAProxy/clearnet endpoint + if [ -n "$domain" ]; then + json_add_object + json_add_string "type" "clearnet" + json_add_string "url" "https://${domain}" + json_add_string "domain" "$domain" + + # Check health + local cert_file="/etc/haproxy/certs/${domain}.pem" + if [ -f "$cert_file" ]; then + json_add_string "status" "active" + json_add_string "ssl" "valid" + else + json_add_string "status" "no_cert" + json_add_string "ssl" "missing" + fi + json_add_string "icon" "🌐" + json_close_object + fi + + # Tor endpoint + if [ -n "$onion" ]; then + json_add_object + json_add_string "type" "tor" + json_add_string "url" "http://${onion}" + json_add_string "onion" "$onion" + json_add_string "status" "active" + json_add_string "icon" "πŸ§…" + json_close_object + fi + + json_close_array + json_dump +} + +# Full publish pipeline - build, deploy to all configured endpoints +full_publish() { + read input + json_load "$input" + json_get_var instance instance + json_get_var rebuild rebuild + + json_init + + if [ -z "$instance" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance required" + json_dump + return + fi + + local data_path=$(uci_get main.data_path) || data_path="$DATA_PATH" + local site_path="$data_path/instances/$instance/site" + + json_add_array "steps" + + # Step 1: Rebuild if requested or needed + if [ "$rebuild" = "1" ] || [ ! -d "$site_path/public" ]; then + json_add_object + json_add_string "step" "build" + json_add_string "status" "running" + json_close_object + + local build_output=$("$HEXOCTL" build "$instance" 2>&1) + local build_result=$? + + json_add_object + json_add_string "step" "build" + if [ "$build_result" -eq 0 ]; then + json_add_string "status" "success" + else + json_add_string "status" "failed" + json_add_string "error" "$build_output" + fi + json_close_object + fi + + # Step 2: Refresh HAProxy if configured + local domain=$(uci -q get hexojs.${instance}.domain) + if [ -n "$domain" ]; then + json_add_object + json_add_string "step" "haproxy" + json_add_string "status" "active" + json_add_string "url" "https://${domain}" + json_close_object + fi + + # Step 3: Refresh Tor if configured + local onion=$(uci -q get hexojs.${instance}.onion_address) + if [ -n "$onion" ]; then + json_add_object + json_add_string "step" "tor" + json_add_string "status" "active" + json_add_string "url" "http://${onion}" + json_close_object + fi + + json_close_array + + json_add_boolean "success" 1 + json_add_string "message" "Publishing pipeline complete" + + json_dump +} + +# ============================================ +# Gitea Webhook Handler +# ============================================ + +# Handle Gitea webhook push event +handle_webhook() { + read input + json_load "$input" + json_get_var event event + json_get_var repository repository + json_get_var ref ref + json_get_var secret secret + + json_init + + # Verify webhook secret if configured + local configured_secret=$(uci -q get hexojs.gitea.webhook_secret) + if [ -n "$configured_secret" ] && [ "$secret" != "$configured_secret" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Invalid webhook secret" + json_dump + return + fi + + # Only handle push events + if [ "$event" != "push" ]; then + json_add_boolean "success" 1 + json_add_string "message" "Event '$event' ignored" + json_dump + return + fi + + local content_repo=$(uci -q get hexojs.gitea.content_repo) + + # Check if this is our content repo + if [ -n "$repository" ] && [ "$repository" != "$content_repo" ]; then + json_add_boolean "success" 1 + json_add_string "message" "Repository '$repository' not configured for sync" + json_dump + return + fi + + # Trigger sync and rebuild + local sync_output=$("$HEXOCTL" gitea sync 2>&1) + local sync_result=$? + + if [ "$sync_result" -eq 0 ]; then + # Auto-rebuild if configured + local auto_build=$(uci -q get hexojs.gitea.auto_build) + if [ "$auto_build" = "1" ]; then + "$HEXOCTL" build 2>&1 & + fi + + json_add_boolean "success" 1 + json_add_string "message" "Content synced from Gitea" + json_add_string "ref" "$ref" + + # Log the event + logger -t hexojs "Webhook: Synced content from Gitea push to $ref" + else + json_add_boolean "success" 0 + json_add_string "error" "Sync failed: $sync_output" + fi + + json_dump +} + +# Configure Gitea webhook +setup_webhook() { + read input + json_load "$input" + json_get_var auto_build auto_build + json_get_var webhook_secret webhook_secret + + json_init + + [ -n "$auto_build" ] && uci set hexojs.gitea.auto_build="$auto_build" + [ -n "$webhook_secret" ] && uci set hexojs.gitea.webhook_secret="$webhook_secret" + uci commit hexojs + + # Generate webhook URL + local lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.255.1") + local webhook_url="http://${lan_ip}/cgi-bin/luci/admin/services/hexojs/webhook" + + json_add_boolean "success" 1 + json_add_string "message" "Webhook configured" + json_add_string "webhook_url" "$webhook_url" + json_add_string "hint" "Add this URL to Gitea repository settings > Webhooks" + + json_dump +} + +# ============================================ +# Instance Health & Pipeline Status +# ============================================ + +# Get detailed health status for an instance +get_instance_health() { + read input + json_load "$input" + json_get_var instance instance + + json_init + + if [ -z "$instance" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Instance required" + json_dump + return + fi + + local data_path=$(uci_get main.data_path) || data_path="$DATA_PATH" + local site_path="$data_path/instances/$instance/site" + local port=$(uci -q get hexojs.${instance}.port) + local domain=$(uci -q get hexojs.${instance}.domain) + local onion=$(uci -q get hexojs.${instance}.onion_address) + + json_add_boolean "success" 1 + json_add_string "instance" "$instance" + + # Overall health score (0-100) + local health_score=100 + local issues="" + + # Check container + json_add_object "container" + if is_running; then + json_add_string "status" "running" + json_add_boolean "healthy" 1 + else + json_add_string "status" "stopped" + json_add_boolean "healthy" 0 + health_score=$((health_score - 30)) + issues="$issues container_stopped" + fi + json_close_object + + # Check site files + json_add_object "site" + if [ -d "$site_path" ]; then + json_add_boolean "exists" 1 + local post_count=$(find "$site_path/source/_posts" -type f -name "*.md" ! -name "index.md" 2>/dev/null | wc -l) + json_add_int "post_count" "$post_count" + + if [ -d "$site_path/public" ]; then + json_add_boolean "built" 1 + local public_files=$(find "$site_path/public" -type f 2>/dev/null | wc -l) + json_add_int "public_files" "$public_files" + else + json_add_boolean "built" 0 + health_score=$((health_score - 20)) + issues="$issues not_built" + fi + else + json_add_boolean "exists" 0 + health_score=$((health_score - 40)) + issues="$issues no_site" + fi + json_close_object + + # Check server + json_add_object "server" + if [ -n "$port" ]; then + json_add_int "port" "$port" + # Check if port is listening + if netstat -tln 2>/dev/null | grep -q ":${port}[[:space:]]"; then + json_add_boolean "listening" 1 + else + json_add_boolean "listening" 0 + health_score=$((health_score - 20)) + issues="$issues not_listening" + fi + fi + json_close_object + + # Check HAProxy + json_add_object "haproxy" + if [ -n "$domain" ]; then + json_add_boolean "configured" 1 + json_add_string "domain" "$domain" + + # Check certificate + local cert_file="/etc/haproxy/certs/${domain}.pem" + if [ -f "$cert_file" ]; then + local expiry_epoch=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | sed 's/notAfter=//' | xargs -I{} date -d "{}" +%s 2>/dev/null || echo 0) + local now_epoch=$(date +%s) + local days_left=$(( (expiry_epoch - now_epoch) / 86400 )) + + if [ "$days_left" -lt 0 ]; then + json_add_string "cert_status" "expired" + health_score=$((health_score - 20)) + issues="$issues cert_expired" + elif [ "$days_left" -lt 7 ]; then + json_add_string "cert_status" "critical" + health_score=$((health_score - 10)) + issues="$issues cert_expiring" + elif [ "$days_left" -lt 30 ]; then + json_add_string "cert_status" "warning" + else + json_add_string "cert_status" "valid" + fi + json_add_int "cert_days_left" "$days_left" + else + json_add_string "cert_status" "missing" + health_score=$((health_score - 10)) + issues="$issues no_cert" + fi + else + json_add_boolean "configured" 0 + fi + json_close_object + + # Check Tor + json_add_object "tor" + if [ -n "$onion" ]; then + json_add_boolean "configured" 1 + json_add_string "onion_address" "$onion" + + # Check if Tor service file exists + if [ -f "/var/lib/tor/hidden_service_hexo_${instance}/hostname" ]; then + json_add_string "status" "active" + else + json_add_string "status" "pending" + fi + else + json_add_boolean "configured" 0 + fi + json_close_object + + # Check Gitea sync + json_add_object "gitea" + if [ -d "$site_path/.git" ]; then + json_add_boolean "configured" 1 + cd "$site_path" + local behind=$(git rev-list --count HEAD..@{u} 2>/dev/null || echo "0") + local modified=$(git status --porcelain 2>/dev/null | wc -l) + + if [ "$behind" -gt 0 ]; then + json_add_string "sync_status" "behind" + json_add_int "commits_behind" "$behind" + issues="$issues sync_behind" + elif [ "$modified" -gt 0 ]; then + json_add_string "sync_status" "modified" + json_add_int "local_changes" "$modified" + else + json_add_string "sync_status" "synced" + fi + else + json_add_boolean "configured" 0 + fi + json_close_object + + # Final health score + [ "$health_score" -lt 0 ] && health_score=0 + json_add_int "health_score" "$health_score" + + if [ "$health_score" -ge 80 ]; then + json_add_string "health_status" "healthy" + elif [ "$health_score" -ge 50 ]; then + json_add_string "health_status" "degraded" + else + json_add_string "health_status" "critical" + fi + + json_add_string "issues" "$issues" + + json_dump +} + +# Get pipeline status for all instances +get_pipeline_status() { + json_init + + local data_path=$(uci_get main.data_path) || data_path="$DATA_PATH" + + json_add_array "instances" + + # Iterate through UCI instances + for instance in $(uci -q show hexojs | grep "=instance" | sed 's/hexojs\.\([^=]*\)=instance/\1/'); do + local port=$(uci -q get hexojs.${instance}.port) + local enabled=$(uci -q get hexojs.${instance}.enabled) + local domain=$(uci -q get hexojs.${instance}.domain) + local onion=$(uci -q get hexojs.${instance}.onion_address) + local profile=$(uci -q get hexojs.${instance}.profile) + local site_path="$data_path/instances/$instance/site" + + json_add_object + json_add_string "id" "$instance" + json_add_int "port" "$port" + json_add_boolean "enabled" "${enabled:-0}" + json_add_string "profile" "${profile:-default}" + + # Quick status checks + local status="unknown" + if [ -d "$site_path" ]; then + # Check if server is running on this port + if netstat -tln 2>/dev/null | grep -q ":${port}[[:space:]]"; then + status="running" + else + status="stopped" + fi + else + status="no_site" + fi + json_add_string "status" "$status" + + # Endpoints + json_add_object "endpoints" + json_add_string "local" "http://localhost:${port}" + [ -n "$domain" ] && json_add_string "clearnet" "https://${domain}" + [ -n "$onion" ] && json_add_string "tor" "http://${onion}" + json_close_object + + json_close_object + done + + json_close_array + + # Global stats + json_add_object "stats" + local total=$(uci -q show hexojs | grep -c "=instance" || echo 0) + local running=$(netstat -tln 2>/dev/null | grep -c ":400[0-9][[:space:]]" || echo 0) + json_add_int "total_instances" "$total" + json_add_int "running_instances" "$running" + json_close_object + + json_dump +} + # ============================================ # Service Control # ============================================ @@ -1674,7 +2952,22 @@ case "$1" in "gitea_clone": {}, "gitea_sync": {}, "gitea_save_config": {"enabled": "bool", "gitea_url": "str", "gitea_user": "str", "gitea_token": "str", "content_repo": "str", "content_branch": "str", "auto_sync": "bool"}, - "publish_to_www": {"path": "str"} + "publish_to_www": {"path": "str"}, + "get_workflow_status": {}, + "list_profiles": {}, + "apply_profile": {"instance": "str", "profile": "str", "domain": "str", "enable_tor": "bool"}, + "publish_to_haproxy": {"instance": "str", "domain": "str", "acme": "bool"}, + "unpublish_from_haproxy": {"instance": "str"}, + "get_haproxy_status": {"instance": "str"}, + "publish_to_tor": {"instance": "str"}, + "unpublish_from_tor": {"instance": "str"}, + "get_tor_status": {"instance": "str"}, + "get_instance_endpoints": {"instance": "str"}, + "full_publish": {"instance": "str", "rebuild": "bool"}, + "handle_webhook": {"event": "str", "repository": "str", "ref": "str", "secret": "str"}, + "setup_webhook": {"auto_build": "bool", "webhook_secret": "str"}, + "get_instance_health": {"instance": "str"}, + "get_pipeline_status": {} } EOF ;; @@ -1727,6 +3020,21 @@ EOF gitea_sync) gitea_sync ;; gitea_save_config) gitea_save_config ;; publish_to_www) publish_to_www ;; + get_workflow_status) get_workflow_status ;; + list_profiles) list_profiles ;; + apply_profile) apply_profile ;; + publish_to_haproxy) publish_to_haproxy ;; + unpublish_from_haproxy) unpublish_from_haproxy ;; + get_haproxy_status) get_haproxy_status ;; + publish_to_tor) publish_to_tor ;; + unpublish_from_tor) unpublish_from_tor ;; + get_tor_status) get_tor_status ;; + get_instance_endpoints) get_instance_endpoints ;; + full_publish) full_publish ;; + handle_webhook) handle_webhook ;; + setup_webhook) setup_webhook ;; + get_instance_health) get_instance_health ;; + get_pipeline_status) get_pipeline_status ;; *) echo '{"error": "Unknown method"}' ;; esac ;; diff --git a/package/secubox/luci-app-hexojs/root/usr/share/rpcd/acl.d/luci-app-hexojs.json b/package/secubox/luci-app-hexojs/root/usr/share/rpcd/acl.d/luci-app-hexojs.json index 6aad33d2..778236f0 100644 --- a/package/secubox/luci-app-hexojs/root/usr/share/rpcd/acl.d/luci-app-hexojs.json +++ b/package/secubox/luci-app-hexojs/root/usr/share/rpcd/acl.d/luci-app-hexojs.json @@ -22,7 +22,14 @@ "git_status", "git_log", "git_get_credentials", - "gitea_status" + "gitea_status", + "get_workflow_status", + "list_profiles", + "get_haproxy_status", + "get_tor_status", + "get_instance_endpoints", + "get_instance_health", + "get_pipeline_status" ] }, "uci": ["hexojs"] @@ -57,7 +64,15 @@ "gitea_clone", "gitea_sync", "gitea_save_config", - "publish_to_www" + "publish_to_www", + "apply_profile", + "publish_to_haproxy", + "unpublish_from_haproxy", + "publish_to_tor", + "unpublish_from_tor", + "full_publish", + "handle_webhook", + "setup_webhook" ] }, "uci": ["hexojs"] diff --git a/package/secubox/luci-app-metablogizer/README.md b/package/secubox/luci-app-metablogizer/README.md new file mode 100644 index 00000000..b8e8f68a --- /dev/null +++ b/package/secubox/luci-app-metablogizer/README.md @@ -0,0 +1,259 @@ +# πŸ“ MetaBlogizer - Static Site Publisher + +One-click static website hosting with automatic HAProxy vhosts, SSL certificates, and Gitea sync. + +## ✨ Features + +| Feature | Description | +|---------|-------------| +| 🌐 **Auto Vhost** | Creates HAProxy vhost + backend automatically | +| πŸ”’ **ACME SSL** | Automatic Let's Encrypt certificates | +| πŸ“¦ **Gitea Sync** | Pull from Gitea repositories | +| πŸ“€ **File Upload** | Drag & drop file uploads | +| πŸ“Š **Health Status** | DNS, certificate, and publish monitoring | +| πŸ”— **QR Codes** | Share sites with QR codes | + +## πŸš€ Quick Start + +### Create a Site via LuCI + +1. Go to **Services β†’ MetaBlogizer** +2. Click **+ New Site** +3. Fill in: + - **Site Name**: `myblog` + - **Domain**: `blog.example.com` + - **Gitea Repo**: `user/repo` (optional) +4. Click **Create** + +### What Happens Automatically + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ πŸ“ Create Site "myblog" @ blog.example.com β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ 1. πŸ“ Create /srv/metablogizer/sites/myblog/ β”‚ +β”‚ 2. 🌐 Create HAProxy backend (metablog_myblog) β”‚ +β”‚ 3. πŸ”— Create HAProxy vhost (blog.example.com) β”‚ +β”‚ 4. πŸ”’ Request ACME certificate β”‚ +β”‚ 5. πŸ“„ Generate default index.html β”‚ +β”‚ 6. βœ… Site live at https://blog.example.com β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## πŸ“Š Dashboard + +### Web Hosting Status Panel + +The dashboard shows real-time health for all sites: + +| Site | Domain | DNS | Resolved IP | Certificate | Status | +|------|--------|-----|-------------|-------------|--------| +| myblog | blog.example.com | 🌐 ok | 185.220.x.x | πŸ”’ 45d | βœ… published | +| docs | docs.example.com | ❌ failed | - | βšͺ missing | πŸ• pending | + +### Status Indicators + +| Icon | DNS Status | Meaning | +|------|------------|---------| +| 🌐 | ok | DNS resolves to your public IP | +| ⚠️ | private | DNS points to private IP (192.168.x.x) | +| ❗ | mismatch | DNS points to different public IP | +| ❌ | failed | DNS resolution failed | + +| Icon | Cert Status | Meaning | +|------|-------------|---------| +| πŸ”’ | ok | Certificate valid (30+ days) | +| ⚠️ | warning | Certificate expiring (7-30 days) | +| πŸ”΄ | critical | Certificate critical (<7 days) | +| πŸ’€ | expired | Certificate expired | +| βšͺ | missing | No certificate | + +| Icon | Publish Status | Meaning | +|------|----------------|---------| +| βœ… | published | Site enabled with content | +| πŸ• | pending | Site enabled, no content yet | +| πŸ“ | draft | Site disabled | + +## πŸ“ File Management + +### Upload Files + +1. Click **Upload** on a site card +2. Drag & drop files or click to browse +3. Check "Set first HTML as homepage" to use as index.html +4. Click **Upload** + +### Manage Files + +1. Click **Files** on a site card +2. View all uploaded files +3. 🏠 Set any HTML file as homepage +4. πŸ—‘οΈ Delete files + +## πŸ”„ Gitea Sync + +### Setup + +1. Create/edit a site +2. Enter Gitea repository: `username/repo` +3. Click **Sync** to pull latest + +### Auto-Sync + +The site syncs from Gitea on: +- Manual sync button click +- Webhook push (if configured) + +```bash +# Manual sync via CLI +ubus call luci.metablogizer sync_site '{"id":"site_myblog"}' +``` + +## πŸ“€ Share & QR + +Click **Share** on any site to get: +- πŸ“‹ Copy URL to clipboard +- πŸ“± QR code for mobile access +- 🐦 Twitter share +- πŸ’Ό LinkedIn share +- πŸ“˜ Facebook share +- ✈️ Telegram share +- πŸ“± WhatsApp share +- βœ‰οΈ Email share + +## πŸ”§ Configuration + +### UCI Settings + +```bash +# /etc/config/metablogizer + +config metablogizer 'main' + option enabled '1' + option runtime 'auto' # auto | uhttpd | nginx + option sites_root '/srv/metablogizer/sites' + option nginx_container 'nginx' + option gitea_url 'http://192.168.255.1:3000' + +config site 'site_myblog' + option name 'myblog' + option domain 'blog.example.com' + option gitea_repo 'user/myblog' + option ssl '1' + option enabled '1' + option description 'My personal blog' + option port '8901' + option runtime 'uhttpd' +``` + +### Runtime Modes + +| Mode | Description | Use Case | +|------|-------------|----------| +| **uhttpd** | OpenWrt's built-in web server | Default, lightweight | +| **nginx** | Nginx in LXC container | Advanced features | +| **auto** | Auto-detect available runtime | Recommended | + +## πŸ“‘ RPCD API + +### Site Management + +```bash +# List all sites +ubus call luci.metablogizer list_sites + +# Create site +ubus call luci.metablogizer create_site '{ + "name": "myblog", + "domain": "blog.example.com", + "gitea_repo": "user/myblog", + "ssl": "1", + "description": "My blog" +}' + +# Sync from Gitea +ubus call luci.metablogizer sync_site '{"id":"site_myblog"}' + +# Delete site +ubus call luci.metablogizer delete_site '{"id":"site_myblog"}' +``` + +### Health Monitoring + +```bash +# Get hosting status for all sites +ubus call luci.metablogizer get_hosting_status + +# Response: +{ + "success": true, + "public_ip": "185.220.101.12", + "haproxy_status": "running", + "sites": [{ + "id": "site_myblog", + "name": "myblog", + "domain": "blog.example.com", + "dns_status": "ok", + "dns_ip": "185.220.101.12", + "cert_status": "ok", + "cert_days": 45, + "publish_status": "published" + }] +} + +# Check single site health +ubus call luci.metablogizer check_site_health '{"id":"site_myblog"}' +``` + +### File Operations + +```bash +# List files in site +ubus call luci.metablogizer list_files '{"id":"site_myblog"}' + +# Upload file (base64 content) +ubus call luci.metablogizer upload_file '{ + "id": "site_myblog", + "filename": "style.css", + "content": "Ym9keSB7IGJhY2tncm91bmQ6ICNmZmY7IH0=" +}' +``` + +## πŸ“ File Locations + +| Path | Description | +|------|-------------| +| `/etc/config/metablogizer` | UCI configuration | +| `/srv/metablogizer/sites/` | Site content directories | +| `/srv/metablogizer/sites//index.html` | Site homepage | +| `/usr/libexec/rpcd/luci.metablogizer` | RPCD backend | + +## πŸ› οΈ Troubleshooting + +### Site Shows 503 + +1. Check HAProxy is running: `lxc-info -n haproxy` +2. Check backend port is listening +3. Verify uhttpd instance: `uci show uhttpd | grep metablog` + +### DNS Not Resolving + +1. Verify A record points to your public IP +2. Check with: `nslookup blog.example.com` +3. Wait for DNS propagation (up to 48h) + +### Certificate Missing + +1. Ensure DNS resolves correctly first +2. Ensure ports 80/443 accessible from internet +3. Check ACME logs: `logread | grep acme` + +### Gitea Sync Fails + +1. Verify Gitea URL: `uci get metablogizer.main.gitea_url` +2. Check repository exists and is public +3. Test manually: `git clone http://192.168.255.1:3000/user/repo.git` + +## πŸ“œ License + +MIT License - Copyright (C) 2025 CyberMind.fr diff --git a/package/secubox/luci-app-metablogizer/htdocs/luci-static/resources/view/metablogizer/overview.js b/package/secubox/luci-app-metablogizer/htdocs/luci-static/resources/view/metablogizer/overview.js index effaa396..429741fe 100644 --- a/package/secubox/luci-app-metablogizer/htdocs/luci-static/resources/view/metablogizer/overview.js +++ b/package/secubox/luci-app-metablogizer/htdocs/luci-static/resources/view/metablogizer/overview.js @@ -11,19 +11,21 @@ var callCreateSite = rpc.declare({ object: 'luci.metablogizer', method: 'create_ var callUpdateSite = rpc.declare({ object: 'luci.metablogizer', method: 'update_site', params: ['id', 'name', 'domain', 'gitea_repo', 'ssl', 'enabled', 'description'], expect: {} }); var callDeleteSite = rpc.declare({ object: 'luci.metablogizer', method: 'delete_site', params: ['id'], expect: {} }); var callSyncSite = rpc.declare({ object: 'luci.metablogizer', method: 'sync_site', params: ['id'], expect: {} }); +var callGetHostingStatus = rpc.declare({ object: 'luci.metablogizer', method: 'get_hosting_status', expect: {} }); +var callCheckSiteHealth = rpc.declare({ object: 'luci.metablogizer', method: 'check_site_health', params: ['id'], expect: {} }); var SITES_ROOT = '/srv/metablogizer/sites'; -var styles = '.mb-container{max-width:1200px;margin:0 auto}.mb-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem;padding:1rem;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);border-radius:12px;color:#fff}.mb-header h2{margin:0;font-size:1.5rem}.mb-status-pills{display:flex;gap:.75rem}.mb-pill{padding:.4rem .8rem;border-radius:20px;font-size:.85rem;background:rgba(255,255,255,.2)}.mb-pill.active{background:rgba(255,255,255,.95);color:#667eea}.mb-btn-primary{background:#fff;color:#667eea;border:none;padding:.6rem 1.2rem;border-radius:8px;cursor:pointer;font-weight:600;transition:transform .2s}.mb-btn-primary:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.15)}.mb-sites-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:1.25rem}.mb-site-card{background:#fff;border-radius:12px;padding:1.25rem;box-shadow:0 2px 12px rgba(0,0,0,.08);border:1px solid #e8e8e8;transition:transform .2s}.mb-site-card:hover{transform:translateY(-4px);box-shadow:0 8px 24px rgba(0,0,0,.12)}.mb-site-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:.75rem}.mb-site-name{font-size:1.15rem;font-weight:600;color:#333;margin:0}.mb-site-status{padding:.25rem .6rem;border-radius:12px;font-size:.75rem;font-weight:500}.mb-site-status.online{background:#d4edda;color:#155724}.mb-site-status.offline{background:#f8d7da;color:#721c24}.mb-site-domain{color:#667eea;font-size:.9rem;margin-bottom:.5rem;word-break:break-all}.mb-site-domain a{color:inherit;text-decoration:none}.mb-site-domain a:hover{text-decoration:underline}.mb-site-meta{font-size:.8rem;color:#888;margin-bottom:1rem}.mb-site-actions{display:flex;gap:.4rem;flex-wrap:wrap}.mb-btn{padding:.35rem .6rem;border-radius:6px;border:1px solid #ddd;background:#f8f9fa;cursor:pointer;font-size:.8rem;transition:all .2s}.mb-btn:hover{background:#e9ecef}.mb-btn-share{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;border:none}.mb-btn-upload{background:#17a2b8;color:#fff;border:none}.mb-btn-upload:hover{background:#138496}.mb-btn-files{background:#6c757d;color:#fff;border:none}.mb-btn-files:hover{background:#5a6268}.mb-btn-edit{background:#fd7e14;color:#fff;border:none}.mb-btn-edit:hover{background:#e96b02}.mb-btn-sync{background:#28a745;color:#fff;border:none}.mb-btn-sync:hover{background:#218838}.mb-btn-delete,.mb-btn-danger{background:#dc3545;color:#fff;border:none}.mb-btn-delete:hover,.mb-btn-danger:hover{background:#c82333}.mb-empty-state{text-align:center;padding:4rem 2rem;background:#fff;border-radius:12px;border:2px dashed #ddd}.mb-empty-state h3{color:#666;margin-bottom:.5rem}.mb-empty-state p{color:#888;margin-bottom:1.5rem}.mb-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);display:flex;justify-content:center;align-items:center;z-index:10000}.mb-modal{background:#fff;border-radius:16px;max-width:500px;width:90%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.3)}.mb-modal-header{padding:1.25rem;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center}.mb-modal-header h3{margin:0;color:#333}.mb-modal-close{background:none;border:none;font-size:1.5rem;cursor:pointer;color:#888;padding:0;line-height:1}.mb-modal-close:hover{color:#333}.mb-modal-body{padding:1.25rem}.mb-form-group{margin-bottom:1rem}.mb-form-group label{display:block;margin-bottom:.4rem;font-weight:500;color:#333;font-size:.9rem}.mb-form-group input,.mb-form-group textarea{width:100%;padding:.6rem .8rem;border:1px solid #ddd;border-radius:8px;font-size:.95rem;box-sizing:border-box}.mb-form-group input:focus,.mb-form-group textarea:focus{border-color:#667eea;outline:none}.mb-form-group textarea{resize:vertical;min-height:60px}.mb-form-group small{color:#888;font-size:.8rem}.mb-form-checkbox{display:flex;align-items:center;gap:.5rem}.mb-form-checkbox input{width:auto}.mb-modal-footer{padding:1rem 1.25rem;border-top:1px solid #eee;display:flex;justify-content:flex-end;gap:.75rem}.mb-btn-cancel{background:#f8f9fa;color:#333}.mb-btn-submit{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;border:none;font-weight:600}.mb-published-card{text-align:center}.mb-url-box{display:flex;gap:.5rem;margin-bottom:1.25rem}.mb-url-box input{flex:1;padding:.6rem;border:1px solid #ddd;border-radius:8px;font-family:monospace;font-size:.9rem;background:#f8f9fa}.mb-url-box button{padding:.6rem 1rem;border:none;background:#667eea;color:#fff;border-radius:8px;cursor:pointer}.mb-qr-container{margin:1.25rem 0;padding:1rem;background:#f8f9fa;border-radius:12px;display:inline-block}.mb-share-buttons{display:flex;justify-content:center;gap:.75rem;flex-wrap:wrap;margin-top:1.25rem}.mb-share-btn{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;text-decoration:none;color:#fff;font-weight:700;font-size:1.1rem;transition:transform .2s}.mb-share-btn:hover{transform:scale(1.1)}.mb-share-twitter{background:#1da1f2}.mb-share-linkedin{background:#0077b5}.mb-share-facebook{background:#1877f2}.mb-share-telegram{background:#0088cc}.mb-share-whatsapp{background:#25d366}.mb-share-email{background:#666}.mb-dropzone{border:2px dashed #ddd;border-radius:12px;padding:2rem;text-align:center;margin-bottom:1rem;cursor:pointer}.mb-dropzone:hover,.mb-dropzone.dragover{border-color:#667eea;background:rgba(102,126,234,.05)}.mb-dropzone-icon{font-size:2.5rem;margin-bottom:.5rem}.mb-dropzone-text{color:#666}.mb-dropzone-text strong{color:#667eea}.mb-file-list{margin-top:1rem;max-height:200px;overflow-y:auto}.mb-file-item{display:flex;align-items:center;gap:.5rem;padding:.5rem;background:#f8f9fa;border-radius:6px;margin-bottom:.5rem;font-size:.85rem}.mb-file-item-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mb-file-item-size{color:#888;font-size:.8rem}.mb-file-item-actions{display:flex;gap:.25rem}.mb-file-item-btn{background:none;border:none;cursor:pointer;padding:.25rem;font-size:.9rem;opacity:.7}.mb-file-item-btn:hover{opacity:1}.mb-file-item-btn.delete{color:#dc3545}.mb-file-item-btn.home{color:#28a745}.mb-cache-hint{background:#fff3cd;border:1px solid #ffc107;border-radius:8px;padding:.75rem;margin-top:1rem;font-size:.85rem;color:#856404}@media(max-width:600px){.mb-header{flex-direction:column;gap:1rem}.mb-sites-grid{grid-template-columns:1fr}}'; +var styles = '.mb-container{max-width:1200px;margin:0 auto}.mb-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem;padding:1rem;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);border-radius:12px;color:#fff}.mb-header h2{margin:0;font-size:1.5rem}.mb-status-pills{display:flex;gap:.75rem}.mb-pill{padding:.4rem .8rem;border-radius:20px;font-size:.85rem;background:rgba(255,255,255,.2)}.mb-pill.active{background:rgba(255,255,255,.95);color:#667eea}.mb-hosting-panel{background:#fff;border-radius:12px;padding:1.25rem;margin-bottom:1.5rem;box-shadow:0 2px 12px rgba(0,0,0,.08);border:1px solid #e8e8e8}.mb-hosting-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem}.mb-hosting-header h3{margin:0;font-size:1.1rem;color:#333}.mb-hosting-ip{background:#f8f9fa;padding:.5rem 1rem;border-radius:8px;font-family:monospace;font-size:.9rem}.mb-hosting-table{width:100%;border-collapse:collapse}.mb-hosting-table th,.mb-hosting-table td{padding:.6rem .8rem;text-align:left;border-bottom:1px solid #eee}.mb-hosting-table th{font-weight:600;color:#666;font-size:.85rem;text-transform:uppercase}.mb-hosting-table td{font-size:.9rem}.mb-status-badge{display:inline-flex;align-items:center;gap:.35rem;padding:.25rem .6rem;border-radius:12px;font-size:.75rem;font-weight:500}.mb-status-ok{background:#d4edda;color:#155724}.mb-status-warning{background:#fff3cd;color:#856404}.mb-status-error{background:#f8d7da;color:#721c24}.mb-status-pending{background:#cfe2ff;color:#084298}.mb-status-none{background:#e9ecef;color:#6c757d}.mb-dns-ip{font-family:monospace;font-size:.85rem;color:#666}.mb-btn-primary{background:#fff;color:#667eea;border:none;padding:.6rem 1.2rem;border-radius:8px;cursor:pointer;font-weight:600;transition:transform .2s}.mb-btn-primary:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.15)}.mb-sites-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:1.25rem}.mb-site-card{background:#fff;border-radius:12px;padding:1.25rem;box-shadow:0 2px 12px rgba(0,0,0,.08);border:1px solid #e8e8e8;transition:transform .2s}.mb-site-card:hover{transform:translateY(-4px);box-shadow:0 8px 24px rgba(0,0,0,.12)}.mb-site-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:.75rem}.mb-site-name{font-size:1.15rem;font-weight:600;color:#333;margin:0}.mb-site-status{padding:.25rem .6rem;border-radius:12px;font-size:.75rem;font-weight:500}.mb-site-status.online{background:#d4edda;color:#155724}.mb-site-status.offline{background:#f8d7da;color:#721c24}.mb-site-domain{color:#667eea;font-size:.9rem;margin-bottom:.5rem;word-break:break-all}.mb-site-domain a{color:inherit;text-decoration:none}.mb-site-domain a:hover{text-decoration:underline}.mb-site-meta{font-size:.8rem;color:#888;margin-bottom:1rem}.mb-site-actions{display:flex;gap:.4rem;flex-wrap:wrap}.mb-btn{padding:.35rem .6rem;border-radius:6px;border:1px solid #ddd;background:#f8f9fa;cursor:pointer;font-size:.8rem;transition:all .2s}.mb-btn:hover{background:#e9ecef}.mb-btn-share{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;border:none}.mb-btn-upload{background:#17a2b8;color:#fff;border:none}.mb-btn-upload:hover{background:#138496}.mb-btn-files{background:#6c757d;color:#fff;border:none}.mb-btn-files:hover{background:#5a6268}.mb-btn-edit{background:#fd7e14;color:#fff;border:none}.mb-btn-edit:hover{background:#e96b02}.mb-btn-sync{background:#28a745;color:#fff;border:none}.mb-btn-sync:hover{background:#218838}.mb-btn-delete,.mb-btn-danger{background:#dc3545;color:#fff;border:none}.mb-btn-delete:hover,.mb-btn-danger:hover{background:#c82333}.mb-empty-state{text-align:center;padding:4rem 2rem;background:#fff;border-radius:12px;border:2px dashed #ddd}.mb-empty-state h3{color:#666;margin-bottom:.5rem}.mb-empty-state p{color:#888;margin-bottom:1.5rem}.mb-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);display:flex;justify-content:center;align-items:center;z-index:10000}.mb-modal{background:#fff;border-radius:16px;max-width:500px;width:90%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.3)}.mb-modal-header{padding:1.25rem;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center}.mb-modal-header h3{margin:0;color:#333}.mb-modal-close{background:none;border:none;font-size:1.5rem;cursor:pointer;color:#888;padding:0;line-height:1}.mb-modal-close:hover{color:#333}.mb-modal-body{padding:1.25rem}.mb-form-group{margin-bottom:1rem}.mb-form-group label{display:block;margin-bottom:.4rem;font-weight:500;color:#333;font-size:.9rem}.mb-form-group input,.mb-form-group textarea{width:100%;padding:.6rem .8rem;border:1px solid #ddd;border-radius:8px;font-size:.95rem;box-sizing:border-box}.mb-form-group input:focus,.mb-form-group textarea:focus{border-color:#667eea;outline:none}.mb-form-group textarea{resize:vertical;min-height:60px}.mb-form-group small{color:#888;font-size:.8rem}.mb-form-checkbox{display:flex;align-items:center;gap:.5rem}.mb-form-checkbox input{width:auto}.mb-modal-footer{padding:1rem 1.25rem;border-top:1px solid #eee;display:flex;justify-content:flex-end;gap:.75rem}.mb-btn-cancel{background:#f8f9fa;color:#333}.mb-btn-submit{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;border:none;font-weight:600}.mb-published-card{text-align:center}.mb-url-box{display:flex;gap:.5rem;margin-bottom:1.25rem}.mb-url-box input{flex:1;padding:.6rem;border:1px solid #ddd;border-radius:8px;font-family:monospace;font-size:.9rem;background:#f8f9fa}.mb-url-box button{padding:.6rem 1rem;border:none;background:#667eea;color:#fff;border-radius:8px;cursor:pointer}.mb-qr-container{margin:1.25rem 0;padding:1rem;background:#f8f9fa;border-radius:12px;display:inline-block}.mb-share-buttons{display:flex;justify-content:center;gap:.75rem;flex-wrap:wrap;margin-top:1.25rem}.mb-share-btn{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;text-decoration:none;color:#fff;font-weight:700;font-size:1.1rem;transition:transform .2s}.mb-share-btn:hover{transform:scale(1.1)}.mb-share-twitter{background:#1da1f2}.mb-share-linkedin{background:#0077b5}.mb-share-facebook{background:#1877f2}.mb-share-telegram{background:#0088cc}.mb-share-whatsapp{background:#25d366}.mb-share-email{background:#666}.mb-dropzone{border:2px dashed #ddd;border-radius:12px;padding:2rem;text-align:center;margin-bottom:1rem;cursor:pointer}.mb-dropzone:hover,.mb-dropzone.dragover{border-color:#667eea;background:rgba(102,126,234,.05)}.mb-dropzone-icon{font-size:2.5rem;margin-bottom:.5rem}.mb-dropzone-text{color:#666}.mb-dropzone-text strong{color:#667eea}.mb-file-list{margin-top:1rem;max-height:200px;overflow-y:auto}.mb-file-item{display:flex;align-items:center;gap:.5rem;padding:.5rem;background:#f8f9fa;border-radius:6px;margin-bottom:.5rem;font-size:.85rem}.mb-file-item-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mb-file-item-size{color:#888;font-size:.8rem}.mb-file-item-actions{display:flex;gap:.25rem}.mb-file-item-btn{background:none;border:none;cursor:pointer;padding:.25rem;font-size:.9rem;opacity:.7}.mb-file-item-btn:hover{opacity:1}.mb-file-item-btn.delete{color:#dc3545}.mb-file-item-btn.home{color:#28a745}.mb-cache-hint{background:#fff3cd;border:1px solid #ffc107;border-radius:8px;padding:.75rem;margin-top:1rem;font-size:.85rem;color:#856404}@media(max-width:600px){.mb-header{flex-direction:column;gap:1rem}.mb-sites-grid{grid-template-columns:1fr}}'; return view.extend({ uploadFiles: [], currentSite: null, - load: function() { return Promise.all([callStatus(), callListSites()]); }, + load: function() { return Promise.all([callStatus(), callListSites(), callGetHostingStatus().catch(function() { return {}; })]); }, render: function(data) { - var self = this, status = data[0] || {}, sites = data[1] || []; + var self = this, status = data[0] || {}, sites = data[1] || [], hosting = data[2] || {}; if (!document.getElementById('mb-styles')) { var s = document.createElement('style'); s.id = 'mb-styles'; s.textContent = styles; document.head.appendChild(s); } @@ -33,11 +35,13 @@ return view.extend({ E('h2', {}, _('MetaBlogizer')), E('div', { 'class': 'mb-status-pills' }, [ E('span', { 'class': 'mb-pill active' }, status.detected_runtime || 'uhttpd'), - E('span', { 'class': 'mb-pill' }, (status.site_count || sites.length || 0) + ' ' + _('Sites')) + E('span', { 'class': 'mb-pill' }, (status.site_count || sites.length || 0) + ' ' + _('Sites')), + E('span', { 'class': 'mb-pill' }, (hosting.haproxy_status === 'running' ? '\u{2705}' : '\u{1F534}') + ' HAProxy') ]) ]), E('button', { 'class': 'mb-btn-primary', 'click': ui.createHandlerFn(this, 'showCreateModal') }, _('+ New Site')) ]), + this.renderHostingPanel(hosting, sites), sites.length > 0 ? E('div', { 'class': 'mb-sites-grid' }, sites.map(function(site) { return self.renderSiteCard(site); })) : E('div', { 'class': 'mb-empty-state' }, [ E('div', { 'style': 'font-size:3rem;margin-bottom:1rem' }, '\u{1F310}'), @@ -48,6 +52,79 @@ return view.extend({ ]); }, + renderHostingPanel: function(hosting, sites) { + var hostingSites = hosting.sites || []; + if (hostingSites.length === 0) return E('div'); + + var statusBadge = function(status, text) { + var cls = 'mb-status-badge '; + switch (status) { + case 'ok': cls += 'mb-status-ok'; break; + case 'warning': cls += 'mb-status-warning'; break; + case 'error': case 'failed': case 'expired': case 'critical': case 'mismatch': cls += 'mb-status-error'; break; + case 'pending': case 'missing': cls += 'mb-status-pending'; break; + default: cls += 'mb-status-none'; + } + return E('span', { 'class': cls }, text || status); + }; + + var dnsIcon = function(status) { + switch (status) { + case 'ok': return '\u{1F310}'; + case 'private': return '\u{26A0}'; + case 'mismatch': return '\u{2757}'; + case 'failed': return '\u{274C}'; + default: return '\u{2796}'; + } + }; + + var certIcon = function(status) { + switch (status) { + case 'ok': return '\u{1F512}'; + case 'warning': return '\u{26A0}'; + case 'critical': case 'expired': return '\u{1F534}'; + case 'missing': return '\u{26AA}'; + default: return '\u{2796}'; + } + }; + + var publishIcon = function(status) { + switch (status) { + case 'published': return '\u{2705}'; + case 'pending': return '\u{1F551}'; + case 'draft': return '\u{1F4DD}'; + default: return '\u{2796}'; + } + }; + + return E('div', { 'class': 'mb-hosting-panel' }, [ + E('div', { 'class': 'mb-hosting-header' }, [ + E('h3', {}, '\u{1F310} ' + _('Web Hosting Status')), + hosting.public_ip ? E('div', { 'class': 'mb-hosting-ip' }, _('Public IP: ') + hosting.public_ip) : '' + ]), + E('table', { 'class': 'mb-hosting-table' }, [ + E('thead', {}, E('tr', {}, [ + E('th', {}, _('Site')), + E('th', {}, _('Domain')), + E('th', {}, _('DNS')), + E('th', {}, _('Resolved IP')), + E('th', {}, _('Certificate')), + E('th', {}, _('Status')) + ])), + E('tbody', {}, hostingSites.map(function(site) { + return E('tr', {}, [ + E('td', {}, E('strong', {}, site.name)), + E('td', {}, site.domain ? E('a', { 'href': site.url, 'target': '_blank' }, site.domain) : E('em', { 'style': 'color:#888' }, _('No domain'))), + E('td', {}, statusBadge(site.dns_status, dnsIcon(site.dns_status) + ' ' + (site.dns_status || 'none'))), + E('td', { 'class': 'mb-dns-ip' }, site.dns_ip || '-'), + E('td', {}, statusBadge(site.cert_status, certIcon(site.cert_status) + ' ' + (site.cert_days ? site.cert_days + 'd' : site.cert_status || 'none'))), + E('td', {}, statusBadge(site.publish_status, publishIcon(site.publish_status) + ' ' + (site.publish_status || 'draft'))) + ]); + })) + ]) + ]); + }, + renderSiteCard: function(site) { return E('div', { 'class': 'mb-site-card' }, [ E('div', { 'class': 'mb-site-header' }, [ diff --git a/package/secubox/luci-app-metablogizer/root/usr/libexec/rpcd/luci.metablogizer b/package/secubox/luci-app-metablogizer/root/usr/libexec/rpcd/luci.metablogizer index 8d3319af..09c8b307 100644 --- a/package/secubox/luci-app-metablogizer/root/usr/libexec/rpcd/luci.metablogizer +++ b/package/secubox/luci-app-metablogizer/root/usr/libexec/rpcd/luci.metablogizer @@ -755,6 +755,302 @@ method_get_settings() { json_dump } +# Helper: Check DNS resolution for a domain +check_dns_resolution() { + local domain="$1" + local resolved_ip="" + if command -v nslookup >/dev/null 2>&1; then + resolved_ip=$(nslookup "$domain" 2>/dev/null | grep -A1 "Name:" | grep "Address" | head -1 | awk '{print $2}') + [ -z "$resolved_ip" ] && resolved_ip=$(nslookup "$domain" 2>/dev/null | grep "Address" | tail -1 | awk '{print $2}' | grep -v "^$") + elif command -v host >/dev/null 2>&1; then + resolved_ip=$(host "$domain" 2>/dev/null | grep "has address" | head -1 | awk '{print $4}') + fi + echo "$resolved_ip" +} + +# Helper: Get public IPv4 address +get_public_ipv4() { + local ip="" + ip=$(wget -qO- -T 5 "http://ipv4.icanhazip.com" 2>/dev/null | tr -d '\n') + [ -z "$ip" ] && ip=$(wget -qO- -T 5 "http://api.ipify.org" 2>/dev/null | tr -d '\n') + echo "$ip" +} + +# Helper: Check certificate expiry +check_cert_expiry() { + local domain="$1" + local cert_file="/srv/haproxy/certs/${domain}.pem" + + if [ ! -f "$cert_file" ]; then + cert_file="/etc/acme/${domain}_ecc/${domain}.cer" + [ ! -f "$cert_file" ] && cert_file="/etc/acme/${domain}/${domain}.cer" + fi + + if [ -f "$cert_file" ]; then + local expiry_date + expiry_date=$(openssl x509 -enddate -noout -in "$cert_file" 2>/dev/null | cut -d= -f2) + if [ -n "$expiry_date" ]; then + local expiry_epoch now_epoch days_left + expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null) + now_epoch=$(date +%s) + if [ -n "$expiry_epoch" ]; then + days_left=$(( (expiry_epoch - now_epoch) / 86400 )) + echo "$days_left" + return 0 + fi + fi + fi + return 1 +} + +# Get hosting status for all sites with DNS and cert health +method_get_hosting_status() { + SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT") + + # Get public IP once + local public_ip + public_ip=$(get_public_ipv4) + + json_init + json_add_boolean "success" 1 + json_add_string "public_ip" "$public_ip" + + # HAProxy status + local haproxy_running="stopped" + if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then + haproxy_running="running" + fi + json_add_string "haproxy_status" "$haproxy_running" + + json_add_array "sites" + + config_load "$UCI_CONFIG" + config_foreach _add_site_health site "$public_ip" + + json_close_array + json_dump +} + +_add_site_health() { + local section="$1" + local public_ip="$2" + local name domain ssl enabled has_content port runtime + + config_get name "$section" name "" + config_get domain "$section" domain "" + config_get ssl "$section" ssl "1" + config_get enabled "$section" enabled "1" + config_get port "$section" port "" + config_get runtime "$section" runtime "" + + [ -z "$name" ] && return + + # Check content + has_content="0" + if [ -d "$SITES_ROOT/$name" ] && [ -f "$SITES_ROOT/$name/index.html" ]; then + has_content="1" + fi + + json_add_object + json_add_string "id" "$section" + json_add_string "name" "$name" + json_add_string "domain" "$domain" + json_add_boolean "enabled" "$enabled" + json_add_boolean "has_content" "$has_content" + [ -n "$port" ] && json_add_int "port" "$port" + json_add_string "runtime" "$runtime" + + # DNS check + if [ -n "$domain" ]; then + local resolved_ip + resolved_ip=$(check_dns_resolution "$domain") + if [ -n "$resolved_ip" ]; then + json_add_string "dns_ip" "$resolved_ip" + # Check if resolves to public IP + case "$resolved_ip" in + 10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.2*|172.30.*|172.31.*|192.168.*) + json_add_string "dns_status" "private" + ;; + *) + if [ "$resolved_ip" = "$public_ip" ]; then + json_add_string "dns_status" "ok" + else + json_add_string "dns_status" "mismatch" + fi + ;; + esac + else + json_add_string "dns_status" "failed" + fi + + # Certificate check + if [ "$ssl" = "1" ]; then + local days_left + days_left=$(check_cert_expiry "$domain") + if [ -n "$days_left" ]; then + if [ "$days_left" -lt 0 ]; then + json_add_string "cert_status" "expired" + elif [ "$days_left" -lt 7 ]; then + json_add_string "cert_status" "critical" + elif [ "$days_left" -lt 30 ]; then + json_add_string "cert_status" "warning" + else + json_add_string "cert_status" "ok" + fi + json_add_int "cert_days" "$days_left" + else + json_add_string "cert_status" "missing" + fi + else + json_add_string "cert_status" "none" + fi + else + json_add_string "dns_status" "none" + json_add_string "cert_status" "none" + fi + + # Publish status + local publish_status="draft" + if [ "$enabled" = "1" ] && [ "$has_content" = "1" ]; then + publish_status="published" + elif [ "$enabled" = "1" ]; then + publish_status="pending" + fi + json_add_string "publish_status" "$publish_status" + + # URL + local protocol="http" + [ "$ssl" = "1" ] && protocol="https" + json_add_string "url" "${protocol}://${domain}" + + json_close_object +} + +# Check health for single site +method_check_site_health() { + local id + + read -r input + json_load "$input" + json_get_var id id + + if [ -z "$id" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Missing site id" + json_dump + return + fi + + local name domain ssl + name=$(get_uci "$id" name "") + domain=$(get_uci "$id" domain "") + ssl=$(get_uci "$id" ssl "1") + + if [ -z "$name" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Site not found" + json_dump + return + fi + + SITES_ROOT=$(get_uci main sites_root "$SITES_ROOT") + + # Get public IP + local public_ip + public_ip=$(get_public_ipv4) + + json_init + json_add_boolean "success" 1 + json_add_string "id" "$id" + json_add_string "name" "$name" + json_add_string "domain" "$domain" + json_add_string "public_ip" "$public_ip" + + # DNS check + json_add_object "dns" + if [ -n "$domain" ]; then + local resolved_ip + resolved_ip=$(check_dns_resolution "$domain") + if [ -n "$resolved_ip" ]; then + json_add_string "resolved_ip" "$resolved_ip" + case "$resolved_ip" in + 10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.2*|172.30.*|172.31.*|192.168.*) + json_add_string "status" "private" + json_add_string "message" "DNS points to private IP" + ;; + *) + if [ "$resolved_ip" = "$public_ip" ]; then + json_add_string "status" "ok" + else + json_add_string "status" "mismatch" + json_add_string "expected" "$public_ip" + fi + ;; + esac + else + json_add_string "status" "failed" + json_add_string "message" "DNS resolution failed" + fi + else + json_add_string "status" "none" + fi + json_close_object + + # Certificate check + json_add_object "certificate" + if [ -n "$domain" ] && [ "$ssl" = "1" ]; then + local days_left + days_left=$(check_cert_expiry "$domain") + if [ -n "$days_left" ]; then + json_add_int "days_left" "$days_left" + if [ "$days_left" -lt 0 ]; then + json_add_string "status" "expired" + elif [ "$days_left" -lt 7 ]; then + json_add_string "status" "critical" + elif [ "$days_left" -lt 30 ]; then + json_add_string "status" "warning" + else + json_add_string "status" "ok" + fi + else + json_add_string "status" "missing" + fi + else + json_add_string "status" "none" + fi + json_close_object + + # Content check + json_add_object "content" + if [ -d "$SITES_ROOT/$name" ]; then + json_add_boolean "exists" 1 + local file_count + file_count=$(find "$SITES_ROOT/$name" -type f 2>/dev/null | wc -l) + json_add_int "file_count" "$file_count" + if [ -f "$SITES_ROOT/$name/index.html" ]; then + json_add_boolean "has_index" 1 + else + json_add_boolean "has_index" 0 + fi + else + json_add_boolean "exists" 0 + fi + json_close_object + + # HAProxy status + json_add_object "haproxy" + if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then + json_add_string "status" "running" + else + json_add_string "status" "stopped" + fi + json_close_object + + json_dump +} + # Save global settings method_save_settings() { local enabled runtime nginx_container sites_root gitea_url @@ -798,25 +1094,29 @@ case "$1" in "upload_file": { "id": "string", "filename": "string", "content": "string" }, "list_files": { "id": "string" }, "get_settings": {}, - "save_settings": { "enabled": "boolean", "nginx_container": "string", "sites_root": "string" } + "save_settings": { "enabled": "boolean", "nginx_container": "string", "sites_root": "string" }, + "get_hosting_status": {}, + "check_site_health": { "id": "string" } } EOF ;; call) case "$2" in - status) method_status ;; - list_sites) method_list_sites ;; - get_site) method_get_site ;; - create_site) method_create_site ;; - update_site) method_update_site ;; - delete_site) method_delete_site ;; - sync_site) method_sync_site ;; - get_publish_info) method_get_publish_info ;; - upload_file) method_upload_file ;; - list_files) method_list_files ;; - get_settings) method_get_settings ;; - save_settings) method_save_settings ;; - *) echo '{"error": "unknown method"}' ;; + status) method_status ;; + list_sites) method_list_sites ;; + get_site) method_get_site ;; + create_site) method_create_site ;; + update_site) method_update_site ;; + delete_site) method_delete_site ;; + sync_site) method_sync_site ;; + get_publish_info) method_get_publish_info ;; + upload_file) method_upload_file ;; + list_files) method_list_files ;; + get_settings) method_get_settings ;; + save_settings) method_save_settings ;; + get_hosting_status) method_get_hosting_status ;; + check_site_health) method_check_site_health ;; + *) echo '{"error": "unknown method"}' ;; esac ;; esac diff --git a/package/secubox/luci-app-metablogizer/root/usr/share/rpcd/acl.d/luci-app-metablogizer.json b/package/secubox/luci-app-metablogizer/root/usr/share/rpcd/acl.d/luci-app-metablogizer.json index 357a54ba..280971c5 100644 --- a/package/secubox/luci-app-metablogizer/root/usr/share/rpcd/acl.d/luci-app-metablogizer.json +++ b/package/secubox/luci-app-metablogizer/root/usr/share/rpcd/acl.d/luci-app-metablogizer.json @@ -8,7 +8,9 @@ "list_sites", "get_site", "get_publish_info", - "get_settings" + "get_settings", + "get_hosting_status", + "check_site_health" ], "file": ["read", "list", "stat"] }, diff --git a/package/secubox/luci-app-mitmproxy/README.md b/package/secubox/luci-app-mitmproxy/README.md new file mode 100644 index 00000000..83fab7d7 --- /dev/null +++ b/package/secubox/luci-app-mitmproxy/README.md @@ -0,0 +1,377 @@ +# πŸ” mitmproxy - HTTPS Interception Proxy + +Interactive HTTPS proxy for debugging, testing, and security analysis with transparent mode support and web-based traffic inspection. + +## ✨ Features + +| Feature | Description | +|---------|-------------| +| πŸ” **Traffic Inspection** | View and analyze HTTP/HTTPS requests in real-time | +| πŸ–₯️ **Web UI** | Built-in mitmweb interface for visual traffic analysis | +| 🎭 **Transparent Mode** | Intercept traffic automatically via nftables | +| πŸ“œ **CA Certificate** | Generate and manage SSL interception certificates | +| πŸ“Š **Statistics** | Track requests, unique hosts, and flow data | +| πŸ”„ **Request Replay** | Replay captured requests for testing | +| βš™οΈ **Filtering** | Filter and track CDN, media, ads, and trackers | +| πŸ›‘οΈ **Whitelist** | Bypass interception for specific IPs/domains | + +## πŸš€ Quick Start + +### Proxy Modes + +| Mode | Icon | Description | Use Case | +|------|------|-------------|----------| +| 🎯 **Regular** | Configure clients manually | Testing specific apps | +| 🎭 **Transparent** | Auto-intercept via firewall | Network-wide inspection | +| ⬆️ **Upstream** | Forward to another proxy | Proxy chaining | +| ⬇️ **Reverse** | Reverse proxy mode | Backend analysis | + +### Enable Transparent Mode + +1. Go to **Security β†’ mitmproxy β†’ Settings** +2. Set **Proxy Mode** to `Transparent` +3. Enable **Transparent Firewall** +4. Click **Save & Apply** + +### Install CA Certificate + +For HTTPS interception, install the mitmproxy CA on client devices: + +1. Configure device to use proxy (or use transparent mode) +2. Navigate to `http://mitm.it` from the device +3. Download and install the certificate for your OS +4. Trust the certificate in system settings + +## πŸ“Š Dashboard + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ πŸ” mitmproxy 🟒 Running β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ πŸ“Š 12.5K β”‚ β”‚ 🌐 245 β”‚ β”‚ πŸ’Ύ 45 MB β”‚ β”‚ πŸ”Œ 8080β”‚ β”‚ +β”‚ β”‚ Requests β”‚ β”‚ Hosts β”‚ β”‚ Flow Data β”‚ β”‚ Port β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ 🌐 Top Hosts πŸ”’ CA Certificate β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ +β”‚ β”‚ πŸ”— api.example.com 1,234 β”‚ β”‚ πŸ“œ mitmproxy CA β”‚β”‚ +β”‚ β”‚ πŸ”— cdn.cloudflare.com 890 β”‚ β”‚ βœ… Certificate installed β”‚β”‚ +β”‚ β”‚ πŸ”— www.google.com 567 β”‚ β”‚ Expires: 2026-01-28 β”‚β”‚ +β”‚ β”‚ πŸ”— analytics.google.com 432 β”‚ β”‚ [⬇ Download] β”‚β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## πŸ” Request Capture + +### Live Request Viewer + +The Requests tab shows captured HTTP traffic in real-time: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ πŸ” Captured Requests ⏸ Pause β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ [GET] api.example.com/users 200 application/json β”‚ +β”‚ [POST] auth.example.com/login 201 application/json β”‚ +β”‚ [GET] cdn.cloudflare.com/script.js 200 text/javascript β”‚ +β”‚ [GET] www.google.com/search 200 text/html β”‚ +β”‚ [PUT] api.example.com/user/123 204 - β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### View Request Details + +Click on any request to see: +- Full request headers +- Response headers +- Cookies +- Request/response body (if captured) + +## 🎭 Transparent Mode + +### Architecture + +``` + Client Device SecuBox Router +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ β”‚ β”‚ +β”‚ Browser │◀── HTTP/S ──▢│ nftables REDIRECT β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”‚ + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ + β”‚ β”‚ mitmproxy β”‚ β”‚ + β”‚ β”‚ (port 8080) β”‚ β”‚ + β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β”‚ β”‚ β”‚ + β”‚ β–Ό β”‚ + β”‚ Internet β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Firewall Setup + +When transparent mode is enabled, mitmproxy automatically creates nftables rules: + +```bash +# HTTP redirect (port 80 β†’ 8080) +nft add rule inet fw4 prerouting tcp dport 80 redirect to :8080 + +# HTTPS redirect (port 443 β†’ 8080) +nft add rule inet fw4 prerouting tcp dport 443 redirect to :8080 +``` + +## βš™οΈ Configuration + +### UCI Settings + +```bash +# /etc/config/mitmproxy + +config mitmproxy 'main' + option enabled '1' + option mode 'transparent' # regular | transparent | upstream | reverse + option proxy_port '8080' + option web_host '0.0.0.0' + option web_port '8081' + option data_path '/srv/mitmproxy' + option memory_limit '256M' + option ssl_insecure '0' # Accept invalid upstream certs + option anticache '0' # Strip cache headers + option anticomp '0' # Disable compression + option flow_detail '1' # Log detail level (0-4) + +config transparent 'transparent' + option enabled '1' + option interface 'br-lan' + option redirect_http '1' + option redirect_https '1' + option http_port '80' + option https_port '443' + +config whitelist 'whitelist' + option enabled '1' + list bypass_ip '192.168.255.0/24' + list bypass_domain 'banking.com' + +config filtering 'filtering' + option enabled '0' + option log_requests '1' + option filter_cdn '0' + option filter_media '0' + option block_ads '0' + +config capture 'capture' + option save_flows '0' + option capture_request_headers '1' + option capture_response_headers '1' + option capture_request_body '0' + option capture_response_body '0' +``` + +## πŸ“‘ RPCD API + +### Service Control + +| Method | Description | +|--------|-------------| +| `get_status` | Get service status | +| `service_start` | Start mitmproxy | +| `service_stop` | Stop mitmproxy | +| `service_restart` | Restart service | +| `install` | Install mitmproxy container | + +### Configuration + +| Method | Description | +|--------|-------------| +| `get_config` | Get main configuration | +| `get_all_config` | Get all configuration sections | +| `get_transparent_config` | Get transparent mode settings | +| `get_whitelist_config` | Get whitelist settings | +| `get_filtering_config` | Get filtering settings | +| `set_config` | Set configuration value | + +### Statistics & Data + +| Method | Description | +|--------|-------------| +| `get_stats` | Get traffic statistics | +| `get_requests` | Get captured requests | +| `get_top_hosts` | Get most requested hosts | +| `get_ca_info` | Get CA certificate info | +| `clear_data` | Clear captured data | + +### Firewall + +| Method | Description | +|--------|-------------| +| `firewall_setup` | Setup transparent mode rules | +| `firewall_clear` | Remove firewall rules | + +### Example Usage + +```bash +# Get status +ubus call luci.mitmproxy get_status + +# Response: +{ + "enabled": true, + "running": true, + "installed": true, + "docker_available": true, + "web_port": 8081, + "proxy_port": 8080, + "listen_port": 8080, + "web_url": "http://192.168.255.1:8081" +} + +# Get statistics +ubus call luci.mitmproxy get_stats + +# Response: +{ + "total_requests": 12500, + "unique_hosts": 245, + "flow_file_size": 47185920, + "cdn_requests": 3200, + "media_requests": 890, + "blocked_ads": 156 +} + +# Get top hosts +ubus call luci.mitmproxy get_top_hosts '{"limit":10}' + +# Response: +{ + "hosts": [ + { "host": "api.example.com", "count": 1234 }, + { "host": "cdn.cloudflare.com", "count": 890 } + ] +} +``` + +## πŸ”’ CA Certificate + +### Generate New Certificate + +```bash +# Certificate is auto-generated on first start +# Located at: /srv/mitmproxy/certs/mitmproxy-ca-cert.pem +``` + +### Download Certificate + +1. Access mitmweb UI at `http://192.168.255.1:8081` +2. Or navigate to `http://mitm.it` from a proxied device +3. Download certificate for your platform + +### Certificate Locations + +| Path | Description | +|------|-------------| +| `/srv/mitmproxy/certs/mitmproxy-ca.pem` | CA private key + certificate | +| `/srv/mitmproxy/certs/mitmproxy-ca-cert.pem` | CA certificate only | +| `/srv/mitmproxy/certs/mitmproxy-ca-cert.cer` | Certificate (DER format) | + +## πŸ›‘οΈ Filtering & Analytics + +### CDN Tracking + +Track traffic to major CDN providers: +- Cloudflare +- Akamai +- Fastly +- AWS CloudFront +- Google Cloud CDN + +### Media Streaming Tracking + +Track streaming services: +- YouTube +- Netflix +- Spotify +- Twitch +- Amazon Prime Video + +### Ad Blocking + +Block known advertising and tracking domains with the built-in filter addon. + +## πŸ“ File Locations + +| Path | Description | +|------|-------------| +| `/etc/config/mitmproxy` | UCI configuration | +| `/srv/mitmproxy/` | Data directory | +| `/srv/mitmproxy/certs/` | CA certificates | +| `/srv/mitmproxy/flows/` | Captured flow files | +| `/var/lib/lxc/mitmproxy/` | LXC container root | +| `/usr/libexec/rpcd/luci.mitmproxy` | RPCD backend | + +## πŸ› οΈ Troubleshooting + +### Service Won't Start + +```bash +# Check container status +lxc-info -n mitmproxy + +# Check logs +logread | grep mitmproxy + +# Verify Docker is available +docker ps +``` + +### No Traffic Being Captured + +1. **Regular mode**: Verify client proxy settings point to `192.168.255.1:8080` +2. **Transparent mode**: Check firewall rules with `nft list ruleset | grep redirect` +3. Verify mitmproxy is listening: `netstat -tln | grep 8080` + +### HTTPS Interception Not Working + +1. Install CA certificate on client device +2. Trust the certificate in system settings +3. Some apps use certificate pinning and cannot be intercepted + +### Web UI Not Accessible + +```bash +# Check web port is listening +netstat -tln | grep 8081 + +# Verify from router +curl -I http://127.0.0.1:8081 + +# Check firewall allows access +uci show firewall | grep mitmproxy +``` + +### Memory Issues + +```bash +# Increase memory limit +uci set mitmproxy.main.memory_limit='512M' +uci commit mitmproxy +/etc/init.d/mitmproxy restart +``` + +## πŸ”’ Security Notes + +1. **Sensitive Tool** - mitmproxy can intercept all network traffic including passwords. Use responsibly. +2. **CA Certificate** - Protect the CA private key. Anyone with access can intercept traffic. +3. **Whitelist Banking** - Add banking and financial sites to the bypass list. +4. **Disable When Not Needed** - Turn off transparent mode when not actively debugging. +5. **Audit Trail** - All captured requests may contain sensitive data. + +## πŸ“œ License + +MIT License - Copyright (C) 2025 CyberMind.fr diff --git a/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/overview.js b/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/overview.js index 9145377e..a09ed676 100644 --- a/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/overview.js +++ b/package/secubox/luci-app-mitmproxy/htdocs/luci-static/resources/view/mitmproxy/overview.js @@ -9,88 +9,367 @@ var callStart = rpc.declare({ object: 'luci.mitmproxy', method: 'start', expect: var callStop = rpc.declare({ object: 'luci.mitmproxy', method: 'stop', expect: {} }); var callRestart = rpc.declare({ object: 'luci.mitmproxy', method: 'restart', expect: {} }); -var css = '.mp-container{max-width:900px;margin:0 auto}.mp-header{display:flex;justify-content:space-between;align-items:center;padding:1.5rem;background:linear-gradient(135deg,#f97316 0%,#ea580c 100%);border-radius:16px;color:#fff;margin-bottom:1.5rem}.mp-header h2{margin:0;font-size:1.5rem;display:flex;align-items:center;gap:.5rem}.mp-status{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;border-radius:20px;font-size:.9rem}.mp-status.running{background:rgba(16,185,129,.2)}.mp-status.stopped{background:rgba(239,68,68,.2)}.mp-dot{width:10px;height:10px;border-radius:50%;animation:pulse 2s infinite}.mp-status.running .mp-dot{background:#10b981}.mp-status.stopped .mp-dot{background:#ef4444}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}.mp-card{background:#fff;border-radius:12px;padding:1.5rem;box-shadow:0 2px 8px rgba(0,0,0,.08);margin-bottom:1rem}.mp-card-title{font-size:1.1rem;font-weight:600;margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.mp-info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem}.mp-info-item{padding:1rem;background:#f8f9fa;border-radius:8px}.mp-info-label{font-size:.8rem;color:#666;margin-bottom:.25rem}.mp-info-value{font-size:1rem;font-weight:500}.mp-actions{display:flex;gap:.75rem;flex-wrap:wrap}.mp-btn{padding:.6rem 1.2rem;border-radius:8px;border:none;cursor:pointer;font-weight:500;transition:all .2s}.mp-btn-primary{background:linear-gradient(135deg,#f97316,#ea580c);color:#fff}.mp-btn-success{background:#10b981;color:#fff}.mp-btn-danger{background:#ef4444;color:#fff}.mp-btn:disabled{opacity:.5;cursor:not-allowed}.mp-not-installed{text-align:center;padding:3rem}.mp-features{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1rem;margin:1.5rem 0}.mp-feature{padding:.75rem;background:#fff7ed;border-radius:8px;font-size:.9rem}.mp-warning{background:#fef3c7;border:1px solid #f59e0b;border-radius:8px;padding:1rem;margin-top:1rem;font-size:.9rem;color:#92400e}'; +var css = [ + ':root { --mp-primary: #e74c3c; --mp-primary-light: #ec7063; --mp-secondary: #3498db; --mp-success: #27ae60; --mp-warning: #f39c12; --mp-danger: #c0392b; --mp-bg: #0d0d12; --mp-card: #141419; --mp-border: rgba(255,255,255,0.08); --mp-text: #e0e0e8; --mp-muted: #8a8a9a; }', + '.mp-overview { max-width: 1000px; margin: 0 auto; padding: 20px; font-family: system-ui, -apple-system, sans-serif; color: var(--mp-text); }', + + /* Header */ + '.mp-header { display: flex; justify-content: space-between; align-items: center; padding: 24px; background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); border-radius: 16px; color: #fff; margin-bottom: 24px; }', + '.mp-header-left { display: flex; align-items: center; gap: 16px; }', + '.mp-logo { font-size: 48px; }', + '.mp-title { font-size: 28px; font-weight: 700; margin: 0; }', + '.mp-subtitle { font-size: 14px; opacity: 0.9; margin-top: 4px; }', + '.mp-status { display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 20px; font-size: 14px; font-weight: 500; }', + '.mp-status.running { background: rgba(39,174,96,0.3); }', + '.mp-status.stopped { background: rgba(239,68,68,0.3); }', + '.mp-dot { width: 10px; height: 10px; border-radius: 50%; animation: pulse 2s infinite; }', + '.mp-status.running .mp-dot { background: #27ae60; }', + '.mp-status.stopped .mp-dot { background: #ef4444; }', + '@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }', + + /* Welcome Banner - shown when not installed/running */ + '.mp-welcome { text-align: center; padding: 60px 40px; background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 16px; margin-bottom: 24px; }', + '.mp-welcome-icon { font-size: 80px; margin-bottom: 20px; }', + '.mp-welcome h2 { font-size: 28px; margin: 0 0 12px 0; color: #fff; }', + '.mp-welcome p { font-size: 16px; color: var(--mp-muted); margin: 0 0 30px 0; max-width: 600px; margin-left: auto; margin-right: auto; }', + '.mp-welcome-note { background: rgba(231,76,60,0.1); border: 1px solid rgba(231,76,60,0.3); border-radius: 12px; padding: 16px; margin-top: 24px; font-size: 14px; color: #ec7063; }', + + /* Mode Cards */ + '.mp-modes { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-bottom: 24px; }', + '.mp-mode-card { background: var(--mp-card); border: 2px solid var(--mp-border); border-radius: 16px; padding: 24px; text-align: center; cursor: pointer; transition: all 0.3s; }', + '.mp-mode-card:hover { border-color: var(--mp-primary); transform: translateY(-4px); box-shadow: 0 8px 32px rgba(231,76,60,0.2); }', + '.mp-mode-card.recommended { border-color: var(--mp-primary); background: linear-gradient(180deg, rgba(231,76,60,0.1) 0%, transparent 100%); }', + '.mp-mode-icon { font-size: 48px; margin-bottom: 16px; }', + '.mp-mode-title { font-size: 18px; font-weight: 600; color: #fff; margin-bottom: 8px; }', + '.mp-mode-desc { font-size: 13px; color: var(--mp-muted); line-height: 1.5; }', + '.mp-mode-badge { display: inline-block; background: var(--mp-primary); color: #fff; font-size: 11px; padding: 4px 10px; border-radius: 12px; margin-top: 12px; text-transform: uppercase; font-weight: 600; }', + + /* Feature Grid */ + '.mp-features { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 24px; }', + '.mp-feature { display: flex; align-items: center; gap: 12px; padding: 16px; background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 12px; }', + '.mp-feature-icon { font-size: 24px; }', + '.mp-feature-text { font-size: 14px; color: var(--mp-text); }', + + /* Quick Actions */ + '.mp-actions { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-bottom: 24px; }', + '.mp-btn { display: inline-flex; align-items: center; gap: 8px; padding: 14px 28px; border-radius: 12px; border: none; cursor: pointer; font-size: 15px; font-weight: 600; transition: all 0.2s; text-decoration: none; }', + '.mp-btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 16px rgba(0,0,0,0.3); }', + '.mp-btn:disabled { opacity: 0.5; cursor: not-allowed; }', + '.mp-btn-primary { background: linear-gradient(135deg, #e74c3c, #c0392b); color: #fff; }', + '.mp-btn-success { background: linear-gradient(135deg, #27ae60, #1e8449); color: #fff; }', + '.mp-btn-danger { background: linear-gradient(135deg, #e74c3c, #c0392b); color: #fff; }', + '.mp-btn-secondary { background: rgba(255,255,255,0.1); color: var(--mp-text); border: 1px solid var(--mp-border); }', + + /* Quick Start Card */ + '.mp-quickstart { background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 16px; padding: 24px; margin-bottom: 24px; }', + '.mp-quickstart-header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }', + '.mp-quickstart-icon { font-size: 28px; }', + '.mp-quickstart-title { font-size: 20px; font-weight: 600; color: #fff; }', + '.mp-quickstart-steps { display: flex; flex-direction: column; gap: 16px; }', + '.mp-step { display: flex; gap: 16px; align-items: flex-start; }', + '.mp-step-num { width: 32px; height: 32px; background: var(--mp-primary); color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 14px; flex-shrink: 0; }', + '.mp-step-content h4 { margin: 0 0 4px 0; font-size: 15px; color: #fff; }', + '.mp-step-content p { margin: 0; font-size: 13px; color: var(--mp-muted); }', + + /* Info Cards */ + '.mp-info-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 24px; }', + '.mp-info-card { background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 12px; padding: 20px; }', + '.mp-info-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }', + '.mp-info-icon { font-size: 24px; }', + '.mp-info-title { font-size: 16px; font-weight: 600; color: #fff; }', + '.mp-info-value { font-size: 24px; font-weight: 700; color: var(--mp-primary); }', + '.mp-info-label { font-size: 13px; color: var(--mp-muted); }', + + /* How It Works */ + '.mp-howto { background: var(--mp-card); border: 1px solid var(--mp-border); border-radius: 16px; padding: 24px; }', + '.mp-howto-header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }', + '.mp-howto-icon { font-size: 28px; }', + '.mp-howto-title { font-size: 20px; font-weight: 600; color: #fff; }', + '.mp-howto-diagram { background: rgba(0,0,0,0.3); border-radius: 12px; padding: 20px; font-family: monospace; font-size: 13px; line-height: 1.6; overflow-x: auto; }', + '.mp-howto-diagram pre { margin: 0; color: var(--mp-text); }' +].join('\n'); return view.extend({ load: function() { return callStatus(); }, handleInstall: function() { - ui.showModal(_('Installing mitmproxy'), [E('p', { 'class': 'spinning' }, _('Installing...'))]); + ui.showModal(_('Installing mitmproxy'), [ + E('p', { 'class': 'spinning' }, _('Downloading and setting up mitmproxy container...')), + E('p', { 'style': 'color: #888; font-size: 13px;' }, _('This may take a few minutes on first install.')) + ]); callInstall().then(function(r) { ui.hideModal(); - ui.addNotification(null, E('p', r.message || _('Installation started'))); + ui.addNotification(null, E('p', r.message || _('Installation started. Please wait and refresh the page.'))); }).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); }); }, handleStart: function() { - ui.showModal(_('Starting...'), [E('p', { 'class': 'spinning' }, _('Starting...'))]); + ui.showModal(_('Starting mitmproxy'), [E('p', { 'class': 'spinning' }, _('Starting proxy service...'))]); callStart().then(function() { ui.hideModal(); location.reload(); }) .catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); }); }, handleStop: function() { - ui.showModal(_('Stopping...'), [E('p', { 'class': 'spinning' }, _('Stopping...'))]); + ui.showModal(_('Stopping mitmproxy'), [E('p', { 'class': 'spinning' }, _('Stopping proxy service...'))]); callStop().then(function() { ui.hideModal(); location.reload(); }) .catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', e.message), 'error'); }); }, render: function(status) { - if (!document.getElementById('mp-styles')) { - var s = document.createElement('style'); s.id = 'mp-styles'; s.textContent = css; document.head.appendChild(s); + if (!document.getElementById('mp-overview-styles')) { + var s = document.createElement('style'); + s.id = 'mp-overview-styles'; + s.textContent = css; + document.head.appendChild(s); } - if (!status.installed || !status.docker_available) { - return E('div', { 'class': 'mp-container' }, [ + var isInstalled = status.installed && status.docker_available; + var isRunning = status.running; + + // Not installed - show welcome wizard + if (!isInstalled) { + return E('div', { 'class': 'mp-overview' }, [ + // Header E('div', { 'class': 'mp-header' }, [ - E('h2', {}, ['\uD83D\uDD0D ', _('mitmproxy')]), - E('div', { 'class': 'mp-status stopped' }, [E('span', { 'class': 'mp-dot' }), _('Not Installed')]) + E('div', { 'class': 'mp-header-left' }, [ + E('div', { 'class': 'mp-logo' }, 'πŸ”'), + E('div', {}, [ + E('h1', { 'class': 'mp-title' }, 'mitmproxy'), + E('div', { 'class': 'mp-subtitle' }, _('HTTPS Interception Proxy')) + ]) + ]), + E('div', { 'class': 'mp-status stopped' }, [ + E('span', { 'class': 'mp-dot' }), + _('Not Installed') + ]) ]), - E('div', { 'class': 'mp-card' }, [ - E('div', { 'class': 'mp-not-installed' }, [ - E('div', { 'style': 'font-size:4rem;margin-bottom:1rem' }, '\uD83D\uDD0D'), - E('h3', {}, _('mitmproxy')), - E('p', {}, _('Interactive HTTPS proxy for debugging, testing, and security analysis.')), - E('div', { 'class': 'mp-features' }, [ - E('div', { 'class': 'mp-feature' }, '\uD83D\uDCCA Web UI'), - E('div', { 'class': 'mp-feature' }, '\uD83D\uDD12 HTTPS'), - E('div', { 'class': 'mp-feature' }, '\uD83D\uDCDD Logging'), - E('div', { 'class': 'mp-feature' }, '\uD83D\uDD04 Replay'), - E('div', { 'class': 'mp-feature' }, '\u2699 Scripting'), - E('div', { 'class': 'mp-feature' }, '\uD83D\uDCE6 Export') + + // Welcome Banner + E('div', { 'class': 'mp-welcome' }, [ + E('div', { 'class': 'mp-welcome-icon' }, 'πŸ”'), + E('h2', {}, _('Intercept & Analyze Network Traffic')), + E('p', {}, _('mitmproxy is a powerful interactive HTTPS proxy that lets you inspect, modify, and replay HTTP/HTTPS traffic. Perfect for debugging APIs, testing applications, and security analysis.')), + + // Features Grid + E('div', { 'class': 'mp-features', 'style': 'margin-bottom: 30px;' }, [ + E('div', { 'class': 'mp-feature' }, [ + E('span', { 'class': 'mp-feature-icon' }, 'πŸ“Š'), + E('span', { 'class': 'mp-feature-text' }, _('Real-time inspection')) ]), - E('div', { 'class': 'mp-warning' }, _('Note: This is a security analysis tool. Only use for legitimate debugging and testing purposes.')), - !status.docker_available ? E('div', { 'style': 'color:#ef4444;margin:1rem 0' }, _('Docker required')) : '', - E('button', { 'class': 'mp-btn mp-btn-primary', 'style': 'margin-top:1rem', 'click': ui.createHandlerFn(this, 'handleInstall'), 'disabled': !status.docker_available }, _('Install mitmproxy')) + E('div', { 'class': 'mp-feature' }, [ + E('span', { 'class': 'mp-feature-icon' }, 'πŸ”’'), + E('span', { 'class': 'mp-feature-text' }, _('HTTPS decryption')) + ]), + E('div', { 'class': 'mp-feature' }, [ + E('span', { 'class': 'mp-feature-icon' }, 'πŸ–₯️'), + E('span', { 'class': 'mp-feature-text' }, _('Web-based UI')) + ]), + E('div', { 'class': 'mp-feature' }, [ + E('span', { 'class': 'mp-feature-icon' }, '🎭'), + E('span', { 'class': 'mp-feature-text' }, _('Transparent mode')) + ]), + E('div', { 'class': 'mp-feature' }, [ + E('span', { 'class': 'mp-feature-icon' }, 'πŸ”„'), + E('span', { 'class': 'mp-feature-text' }, _('Request replay')) + ]), + E('div', { 'class': 'mp-feature' }, [ + E('span', { 'class': 'mp-feature-icon' }, 'πŸ“'), + E('span', { 'class': 'mp-feature-text' }, _('Flow logging')) + ]) + ]), + + !status.docker_available ? + E('div', { 'style': 'color: #ef4444; margin-bottom: 20px;' }, [ + E('span', { 'style': 'font-size: 24px;' }, '⚠️ '), + _('Docker is required but not available') + ]) : null, + + E('button', { + 'class': 'mp-btn mp-btn-primary', + 'click': ui.createHandlerFn(this, 'handleInstall'), + 'disabled': !status.docker_available + }, ['πŸ“¦ ', _('Install mitmproxy')]), + + E('div', { 'class': 'mp-welcome-note' }, [ + '⚠️ ', + _('Security Note: mitmproxy is a powerful security analysis tool. Only use for legitimate debugging, testing, and security research purposes.') + ]) + ]), + + // Proxy Modes + E('h3', { 'style': 'margin: 0 0 16px 0; font-size: 18px; color: #fff;' }, '🎯 ' + _('Proxy Modes')), + E('div', { 'class': 'mp-modes' }, [ + E('div', { 'class': 'mp-mode-card' }, [ + E('div', { 'class': 'mp-mode-icon' }, '🎯'), + E('div', { 'class': 'mp-mode-title' }, _('Regular Proxy')), + E('div', { 'class': 'mp-mode-desc' }, _('Configure clients to use the proxy manually. Best for testing specific applications.')) + ]), + E('div', { 'class': 'mp-mode-card recommended' }, [ + E('div', { 'class': 'mp-mode-icon' }, '🎭'), + E('div', { 'class': 'mp-mode-title' }, _('Transparent Mode')), + E('div', { 'class': 'mp-mode-desc' }, _('Intercept all network traffic automatically via firewall rules.')), + E('span', { 'class': 'mp-mode-badge' }, _('Recommended')) + ]), + E('div', { 'class': 'mp-mode-card' }, [ + E('div', { 'class': 'mp-mode-icon' }, '⬆️'), + E('div', { 'class': 'mp-mode-title' }, _('Upstream Proxy')), + E('div', { 'class': 'mp-mode-desc' }, _('Forward traffic to another proxy server for proxy chaining.')) + ]), + E('div', { 'class': 'mp-mode-card' }, [ + E('div', { 'class': 'mp-mode-icon' }, '⬇️'), + E('div', { 'class': 'mp-mode-title' }, _('Reverse Proxy')), + E('div', { 'class': 'mp-mode-desc' }, _('Act as a reverse proxy to inspect backend server traffic.')) ]) ]) ]); } - return E('div', { 'class': 'mp-container' }, [ + // Installed - show dashboard overview + return E('div', { 'class': 'mp-overview' }, [ + // Header E('div', { 'class': 'mp-header' }, [ - E('h2', {}, ['\uD83D\uDD0D ', _('mitmproxy')]), - E('div', { 'class': 'mp-status ' + (status.running ? 'running' : 'stopped') }, [ + E('div', { 'class': 'mp-header-left' }, [ + E('div', { 'class': 'mp-logo' }, 'πŸ”'), + E('div', {}, [ + E('h1', { 'class': 'mp-title' }, 'mitmproxy'), + E('div', { 'class': 'mp-subtitle' }, _('HTTPS Interception Proxy')) + ]) + ]), + E('div', { 'class': 'mp-status ' + (isRunning ? 'running' : 'stopped') }, [ E('span', { 'class': 'mp-dot' }), - status.running ? _('Running') : _('Stopped') + isRunning ? _('Running') : _('Stopped') ]) ]), - E('div', { 'class': 'mp-card' }, [ - E('div', { 'class': 'mp-card-title' }, ['\u2139\uFE0F ', _('Configuration')]), - E('div', { 'class': 'mp-info-grid' }, [ - E('div', { 'class': 'mp-info-item' }, [E('div', { 'class': 'mp-info-label' }, _('Proxy Port')), E('div', { 'class': 'mp-info-value' }, String(status.proxy_port))]), - E('div', { 'class': 'mp-info-item' }, [E('div', { 'class': 'mp-info-label' }, _('Web UI Port')), E('div', { 'class': 'mp-info-value' }, String(status.web_port))]), - E('div', { 'class': 'mp-info-item' }, [E('div', { 'class': 'mp-info-label' }, _('Web UI')), E('div', { 'class': 'mp-info-value' }, [E('a', { 'href': 'http://' + window.location.hostname + ':' + status.web_port, 'target': '_blank' }, _('Open UI'))])]) + + // Quick Actions + E('div', { 'class': 'mp-actions' }, [ + isRunning ? E('button', { + 'class': 'mp-btn mp-btn-danger', + 'click': ui.createHandlerFn(this, 'handleStop') + }, ['⏹ ', _('Stop Proxy')]) : + E('button', { + 'class': 'mp-btn mp-btn-success', + 'click': ui.createHandlerFn(this, 'handleStart') + }, ['▢️ ', _('Start Proxy')]), + + E('a', { + 'class': 'mp-btn mp-btn-primary', + 'href': L.url('admin', 'secubox', 'security', 'mitmproxy', 'dashboard') + }, ['πŸ“Š ', _('Dashboard')]), + + E('a', { + 'class': 'mp-btn mp-btn-secondary', + 'href': 'http://' + window.location.hostname + ':' + (status.web_port || 8081), + 'target': '_blank' + }, ['πŸ–₯️ ', _('Web UI')]), + + E('a', { + 'class': 'mp-btn mp-btn-secondary', + 'href': L.url('admin', 'secubox', 'security', 'mitmproxy', 'settings') + }, ['βš™οΈ ', _('Settings')]) + ]), + + // Info Cards + E('div', { 'class': 'mp-info-grid' }, [ + E('div', { 'class': 'mp-info-card' }, [ + E('div', { 'class': 'mp-info-header' }, [ + E('span', { 'class': 'mp-info-icon' }, 'πŸ”Œ'), + E('span', { 'class': 'mp-info-title' }, _('Proxy Port')) + ]), + E('div', { 'class': 'mp-info-value' }, String(status.proxy_port || 8080)), + E('div', { 'class': 'mp-info-label' }, _('HTTP/HTTPS interception')) + ]), + E('div', { 'class': 'mp-info-card' }, [ + E('div', { 'class': 'mp-info-header' }, [ + E('span', { 'class': 'mp-info-icon' }, 'πŸ–₯️'), + E('span', { 'class': 'mp-info-title' }, _('Web UI Port')) + ]), + E('div', { 'class': 'mp-info-value' }, String(status.web_port || 8081)), + E('div', { 'class': 'mp-info-label' }, _('mitmweb interface')) + ]), + E('div', { 'class': 'mp-info-card' }, [ + E('div', { 'class': 'mp-info-header' }, [ + E('span', { 'class': 'mp-info-icon' }, 'πŸ’Ύ'), + E('span', { 'class': 'mp-info-title' }, _('Data Path')) + ]), + E('div', { 'class': 'mp-info-value', 'style': 'font-size: 14px; word-break: break-all;' }, status.data_path || '/srv/mitmproxy'), + E('div', { 'class': 'mp-info-label' }, _('Certificates & flows')) ]) ]), - E('div', { 'class': 'mp-card' }, [ - E('div', { 'class': 'mp-card-title' }, ['\u26A1 ', _('Actions')]), - E('div', { 'class': 'mp-actions' }, [ - E('button', { 'class': 'mp-btn mp-btn-success', 'click': ui.createHandlerFn(this, 'handleStart'), 'disabled': status.running }, _('Start')), - E('button', { 'class': 'mp-btn mp-btn-danger', 'click': ui.createHandlerFn(this, 'handleStop'), 'disabled': !status.running }, _('Stop')) + + // Quick Start Guide + E('div', { 'class': 'mp-quickstart' }, [ + E('div', { 'class': 'mp-quickstart-header' }, [ + E('span', { 'class': 'mp-quickstart-icon' }, 'πŸš€'), + E('span', { 'class': 'mp-quickstart-title' }, _('Quick Start Guide')) + ]), + E('div', { 'class': 'mp-quickstart-steps' }, [ + E('div', { 'class': 'mp-step' }, [ + E('div', { 'class': 'mp-step-num' }, '1'), + E('div', { 'class': 'mp-step-content' }, [ + E('h4', {}, _('Start the Proxy')), + E('p', {}, _('Click the Start button above to begin intercepting traffic.')) + ]) + ]), + E('div', { 'class': 'mp-step' }, [ + E('div', { 'class': 'mp-step-num' }, '2'), + E('div', { 'class': 'mp-step-content' }, [ + E('h4', {}, _('Install CA Certificate')), + E('p', {}, [ + _('Navigate to '), + E('code', { 'style': 'background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 4px;' }, 'http://mitm.it'), + _(' from a proxied device to download and install the CA certificate.') + ]) + ]) + ]), + E('div', { 'class': 'mp-step' }, [ + E('div', { 'class': 'mp-step-num' }, '3'), + E('div', { 'class': 'mp-step-content' }, [ + E('h4', {}, _('Configure Clients')), + E('p', {}, [ + _('Set proxy to '), + E('code', { 'style': 'background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 4px;' }, window.location.hostname + ':' + (status.proxy_port || 8080)), + _(' or enable transparent mode in Settings.') + ]) + ]) + ]), + E('div', { 'class': 'mp-step' }, [ + E('div', { 'class': 'mp-step-num' }, '4'), + E('div', { 'class': 'mp-step-content' }, [ + E('h4', {}, _('View Traffic')), + E('p', {}, _('Open the Dashboard or Web UI to see captured requests in real-time.')) + ]) + ]) + ]) + ]), + + // How It Works Diagram + E('div', { 'class': 'mp-howto' }, [ + E('div', { 'class': 'mp-howto-header' }, [ + E('span', { 'class': 'mp-howto-icon' }, 'πŸ“–'), + E('span', { 'class': 'mp-howto-title' }, _('How mitmproxy Works')) + ]), + E('div', { 'class': 'mp-howto-diagram' }, [ + E('pre', {}, [ + ' Client Device SecuBox Router Internet\n', + ' β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”\n', + ' β”‚ Browser │─────▢│ mitmproxy │─────▢│ Server β”‚\n', + ' β”‚ │◀─────│ │◀─────│ β”‚\n', + ' β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ πŸ” Inspect β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\n', + ' β”‚ ✏️ Modify β”‚\n', + ' β”‚ πŸ“Š Log β”‚\n', + ' β”‚ πŸ”„ Replay β”‚\n', + ' β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\n', + '\n', + ' Port ' + (status.proxy_port || 8080) + ': HTTP/HTTPS interception\n', + ' Port ' + (status.web_port || 8081) + ': Web UI (mitmweb)' + ].join('')) ]) ]) ]); }, - handleSaveApply: null, handleSave: null, handleReset: null + handleSaveApply: null, + handleSave: null, + handleReset: null }); diff --git a/package/secubox/luci-app-service-registry/root/usr/libexec/rpcd/luci.service-registry b/package/secubox/luci-app-service-registry/root/usr/libexec/rpcd/luci.service-registry index 58b166b9..412d0840 100644 --- a/package/secubox/luci-app-service-registry/root/usr/libexec/rpcd/luci.service-registry +++ b/package/secubox/luci-app-service-registry/root/usr/libexec/rpcd/luci.service-registry @@ -976,26 +976,62 @@ get_reverse_dns() { # Helper: Check certificate expiry check_cert_expiry() { local domain="$1" - local cert_file="/srv/haproxy/certs/${domain}.pem" + local cert_file="" - if [ ! -f "$cert_file" ]; then - # Try the ACME path + # Try multiple possible certificate locations + # 1. HAProxy certs directory (combined pem files) + if [ -f "/srv/haproxy/certs/${domain}.pem" ]; then + cert_file="/srv/haproxy/certs/${domain}.pem" + # 2. ACME standard path + elif [ -f "/etc/acme/${domain}/${domain}.cer" ]; then + cert_file="/etc/acme/${domain}/${domain}.cer" + # 3. ACME fullchain (some setups use this) + elif [ -f "/etc/acme/${domain}/fullchain.cer" ]; then + cert_file="/etc/acme/${domain}/fullchain.cer" + # 4. ACME with _ecc suffix (ECC certs) + elif [ -f "/etc/acme/${domain}_ecc/${domain}.cer" ]; then cert_file="/etc/acme/${domain}_ecc/${domain}.cer" - [ ! -f "$cert_file" ] && cert_file="/etc/acme/${domain}/${domain}.cer" + # 5. Let's Encrypt standard path + elif [ -f "/etc/letsencrypt/live/${domain}/cert.pem" ]; then + cert_file="/etc/letsencrypt/live/${domain}/cert.pem" fi - if [ -f "$cert_file" ]; then + if [ -n "$cert_file" ] && [ -f "$cert_file" ]; then # Get expiry date using openssl local expiry_date expiry_date=$(openssl x509 -enddate -noout -in "$cert_file" 2>/dev/null | cut -d= -f2) if [ -n "$expiry_date" ]; then - # Convert to epoch - local expiry_epoch + # Convert to epoch - try multiple date formats for compatibility + local expiry_epoch now_epoch days_left + + # BusyBox date may not support -d with GMT format + # Try direct parsing first expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null) - local now_epoch + + # If that fails, try converting the format + if [ -z "$expiry_epoch" ]; then + # Parse "Apr 27 04:05:21 2026 GMT" format manually + local month day time year + month=$(echo "$expiry_date" | awk '{print $1}') + day=$(echo "$expiry_date" | awk '{print $2}') + time=$(echo "$expiry_date" | awk '{print $3}') + year=$(echo "$expiry_date" | awk '{print $4}') + + # Convert month name to number + case "$month" in + Jan) month="01" ;; Feb) month="02" ;; Mar) month="03" ;; + Apr) month="04" ;; May) month="05" ;; Jun) month="06" ;; + Jul) month="07" ;; Aug) month="08" ;; Sep) month="09" ;; + Oct) month="10" ;; Nov) month="11" ;; Dec) month="12" ;; + esac + + # Try with reformatted date + expiry_epoch=$(date -d "${year}-${month}-${day}" +%s 2>/dev/null) + fi + now_epoch=$(date +%s) - local days_left - if [ -n "$expiry_epoch" ]; then + + if [ -n "$expiry_epoch" ] && [ -n "$now_epoch" ]; then days_left=$(( (expiry_epoch - now_epoch) / 86400 )) echo "$days_left" return 0 diff --git a/package/secubox/luci-app-tor-shield/README.md b/package/secubox/luci-app-tor-shield/README.md new file mode 100644 index 00000000..c63676e3 --- /dev/null +++ b/package/secubox/luci-app-tor-shield/README.md @@ -0,0 +1,281 @@ +# πŸ§… Tor Shield - Anonymous Routing Made Simple + +Network-wide privacy protection through the Tor network with one-click activation. + +## ✨ Features + +### πŸ›‘οΈ Protection Modes + +| Mode | Description | Use Case | +|------|-------------|----------| +| 🌐 **Transparent Proxy** | All network traffic routed through Tor automatically | Full network anonymity | +| 🎯 **SOCKS Proxy** | Apps connect via SOCKS5 (127.0.0.1:9050) | Selective app protection | +| πŸ”“ **Bridge Mode** | Uses obfs4/meek bridges to bypass censorship | Restrictive networks | + +### πŸš€ Quick Start Presets + +| Preset | Icon | Configuration | +|--------|------|---------------| +| **Full Anonymity** | πŸ›‘οΈ | Transparent + DNS over Tor + Kill Switch | +| **Selective Apps** | 🎯 | SOCKS only, no kill switch | +| **Bypass Censorship** | πŸ”“ | Bridges enabled + obfs4 | + +### πŸ”’ Security Features + +- **πŸ” Kill Switch** - Blocks all traffic if Tor disconnects +- **🌍 DNS over Tor** - Prevents DNS leaks +- **πŸ”„ New Identity** - Request fresh circuits instantly +- **πŸ” Leak Test** - Verify your protection is working +- **πŸ§… Hidden Services** - Host .onion sites + +## πŸ“Š Dashboard + +The dashboard provides real-time monitoring: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ πŸ§… Tor Shield 🟒 Protected β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” Your Protection Status β”‚ +β”‚ β”‚ πŸ§… β”‚ ───────────────────────── β”‚ +β”‚ β”‚ Toggle β”‚ Real IP: 192.168.x.x β”‚ +β”‚ β”‚ β”‚ Tor Exit: 185.220.x.x πŸ‡©πŸ‡ͺ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ πŸ›‘οΈ Full β”‚ 🎯 Selective β”‚ πŸ”“ Censored β”‚ β”‚ +β”‚ β”‚ Anonymity β”‚ Apps β”‚ Bypass β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ πŸ”„ Circuits: 5 β”‚ πŸ“Š 45 KB/s β”‚ ⏱ 2h 15m β”‚ +β”‚ πŸ“₯ 125 MB β”‚ πŸ“€ 45 MB β”‚ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚πŸŸ’Serviceβ”‚πŸŸ’Boot β”‚πŸŸ’DNS β”‚πŸŸ’Kill β”‚ β”‚ +β”‚ β”‚ Running β”‚ 100% β”‚Protectedβ”‚ Active β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## πŸ§… Hidden Services + +Host your services on the Tor network with .onion addresses: + +```bash +# Via LuCI +Services β†’ Tor Shield β†’ Hidden Services β†’ Add + +# Via CLI +ubus call luci.tor-shield add_hidden_service '{"name":"mysite","local_port":80,"virtual_port":80}' + +# Get onion address +cat /var/lib/tor/hidden_service_mysite/hostname +``` + +### Example Hidden Services + +| Service | Local Port | Onion Port | Use Case | +|---------|-----------|------------|----------| +| Web Server | 80 | 80 | Anonymous website | +| SSH | 22 | 22 | Secure remote access | +| API | 8080 | 80 | Anonymous API endpoint | + +## πŸŒ‰ Bridges + +Bypass network censorship using Tor bridges: + +### Bridge Types + +| Type | Description | When to Use | +|------|-------------|-------------| +| **obfs4** | Obfuscated protocol | Most censored networks | +| **meek-azure** | Domain fronting via Azure | Highly restrictive networks | +| **snowflake** | WebRTC-based | Dynamic bridge discovery | + +### Auto-Bridge Detection + +```bash +# Enable automatic bridge selection +uci set tor-shield.main.auto_bridges=1 +uci commit tor-shield +/etc/init.d/tor-shield restart +``` + +## πŸ”§ Configuration + +### UCI Settings + +```bash +# /etc/config/tor-shield + +config tor-shield 'main' + option enabled '1' + option mode 'transparent' # transparent | socks + option dns_over_tor '1' # Route DNS through Tor + option kill_switch '1' # Block traffic if Tor fails + option auto_bridges '0' # Auto-detect censorship + +config socks 'socks' + option port '9050' + option address '127.0.0.1' + +config trans 'trans' + option port '9040' + option dns_port '9053' + list excluded_ips '192.168.255.0/24' # LAN bypass + +config bridges 'bridges' + option enabled '0' + option type 'obfs4' + +config security 'security' + option exit_nodes '' # Country codes: {us},{de} + option exclude_exit_nodes '' # Avoid: {ru},{cn} + option strict_nodes '0' + +config hidden_service 'hs_mysite' + option enabled '1' + option name 'mysite' + option local_port '80' + option virtual_port '80' +``` + +## πŸ“‘ RPCD API + +### Status & Control + +```bash +# Get status +ubus call luci.tor-shield status + +# Enable with preset +ubus call luci.tor-shield enable '{"preset":"anonymous"}' + +# Disable +ubus call luci.tor-shield disable + +# Restart +ubus call luci.tor-shield restart + +# Request new identity +ubus call luci.tor-shield new_identity + +# Check for leaks +ubus call luci.tor-shield check_leaks +``` + +### Circuit Management + +```bash +# Get active circuits +ubus call luci.tor-shield circuits + +# Response: +{ + "circuits": [{ + "id": "123", + "status": "BUILT", + "path": "$A~Guard,$B~Middle,$C~Exit", + "purpose": "GENERAL", + "nodes": [ + {"fingerprint": "ABC123", "name": "Guard"}, + {"fingerprint": "DEF456", "name": "Middle"}, + {"fingerprint": "GHI789", "name": "Exit"} + ] + }] +} +``` + +### Hidden Services + +```bash +# List hidden services +ubus call luci.tor-shield hidden_services + +# Add hidden service +ubus call luci.tor-shield add_hidden_service '{"name":"web","local_port":80,"virtual_port":80}' + +# Remove hidden service +ubus call luci.tor-shield remove_hidden_service '{"name":"web"}' +``` + +### Bandwidth Stats + +```bash +# Get bandwidth +ubus call luci.tor-shield bandwidth + +# Response: +{ + "read": 125000000, # Total bytes downloaded + "written": 45000000, # Total bytes uploaded + "read_rate": 45000, # Current download rate (bytes/sec) + "write_rate": 12000 # Current upload rate (bytes/sec) +} +``` + +## πŸ› οΈ Troubleshooting + +### Tor Won't Start + +```bash +# Check logs +logread | grep -i tor + +# Verify config +tor --verify-config -f /var/run/tor/torrc + +# Check control socket +ls -la /var/run/tor/control +``` + +### Slow Connections + +1. **Check bootstrap** - Wait for 100% completion +2. **Try bridges** - Network may be throttling Tor +3. **Change circuits** - Click "New Identity" +4. **Check exit nodes** - Some exits are slow + +### DNS Leaks + +```bash +# Verify DNS is routed through Tor +nslookup check.torproject.org + +# Should resolve via Tor DNS (127.0.0.1:9053) +``` + +### Kill Switch Issues + +```bash +# Check firewall rules +iptables -L -n | grep -i tor + +# Verify kill switch config +uci get tor-shield.main.kill_switch +``` + +## πŸ“ File Locations + +| Path | Description | +|------|-------------| +| `/etc/config/tor-shield` | UCI configuration | +| `/var/run/tor/torrc` | Generated Tor config | +| `/var/run/tor/control` | Control socket | +| `/var/lib/tor/` | Tor data directory | +| `/var/lib/tor/hidden_service_*/` | Hidden service keys | +| `/tmp/tor_exit_ip` | Cached exit IP | +| `/tmp/tor_real_ip` | Cached real IP | + +## πŸ” Security Notes + +1. **Kill Switch** - Always enable for maximum protection +2. **DNS Leaks** - Enable DNS over Tor to prevent leaks +3. **Hidden Services** - Keys in `/var/lib/tor/` are sensitive - back them up securely +4. **Exit Nodes** - Consider excluding certain countries for sensitive use +5. **Bridges** - Use if your ISP blocks or throttles Tor + +## πŸ“œ License + +MIT License - Copyright (C) 2025 CyberMind.fr diff --git a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/overview.js b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/overview.js index 92cebb5e..0bed57ea 100644 --- a/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/overview.js +++ b/package/secubox/luci-app-tor-shield/htdocs/luci-static/resources/view/tor-shield/overview.js @@ -250,20 +250,49 @@ return view.extend({ var statusClass = 'disabled'; var statusText = _('Disabled'); + var statusEmoji = '\u{26AA}'; if (isConnecting) { statusClass = 'connecting'; statusText = _('Connecting %d%%').format(status.bootstrap); + statusEmoji = '\u{1F7E1}'; } else if (isProtected) { statusClass = 'protected'; statusText = _('Protected'); + statusEmoji = '\u{1F7E2}'; } else if (isActive) { statusClass = 'exposed'; statusText = _('Exposed'); + statusEmoji = '\u{1F534}'; } var view = E('div', { 'class': 'tor-dashboard' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('tor-shield/dashboard.css') }), + // Wizard Welcome Banner (shown when disabled) + !isActive ? E('div', { 'class': 'tor-wizard-banner', 'style': 'background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 50%, #06b6d4 100%); border-radius: 16px; padding: 24px; margin-bottom: 20px; color: #fff; text-align: center;' }, [ + E('div', { 'style': 'font-size: 48px; margin-bottom: 12px;' }, '\u{1F9D9}\u200D\u2642\uFE0F'), + E('h2', { 'style': 'margin: 0 0 8px 0; font-size: 24px;' }, '\u{2728} ' + _('Welcome to Tor Shield') + ' \u{2728}'), + E('p', { 'style': 'margin: 0 0 16px 0; opacity: 0.9; font-size: 14px;' }, _('Your gateway to anonymous browsing. Choose a protection level below to get started.')), + E('div', { 'style': 'display: flex; justify-content: center; gap: 24px; flex-wrap: wrap; margin-top: 16px;' }, [ + E('div', { 'style': 'text-align: center;' }, [ + E('div', { 'style': 'font-size: 32px;' }, '\u{1F512}'), + E('div', { 'style': 'font-size: 12px; opacity: 0.8;' }, _('Encrypted')) + ]), + E('div', { 'style': 'text-align: center;' }, [ + E('div', { 'style': 'font-size: 32px;' }, '\u{1F310}'), + E('div', { 'style': 'font-size: 12px; opacity: 0.8;' }, _('Anonymous')) + ]), + E('div', { 'style': 'text-align: center;' }, [ + E('div', { 'style': 'font-size: 32px;' }, '\u{1F6E1}'), + E('div', { 'style': 'font-size: 12px; opacity: 0.8;' }, _('Protected')) + ]), + E('div', { 'style': 'text-align: center;' }, [ + E('div', { 'style': 'font-size: 32px;' }, '\u{1F30D}'), + E('div', { 'style': 'font-size: 12px; opacity: 0.8;' }, _('Worldwide')) + ]) + ]) + ]) : '', + // Header E('div', { 'class': 'tor-header' }, [ E('div', { 'class': 'tor-logo' }, [ @@ -271,7 +300,7 @@ return view.extend({ E('div', { 'class': 'tor-logo-text' }, ['Tor ', E('span', {}, 'Shield')]) ]), E('div', { 'class': 'tor-status-badge ' + statusClass }, [ - E('span', { 'class': 'tor-status-dot' }), + E('span', { 'style': 'margin-right: 6px;' }, statusEmoji), statusText ]) ]), @@ -350,20 +379,40 @@ return view.extend({ ]) ]), - // Presets - E('div', { 'class': 'tor-presets' }, - presets.map(function(preset) { - return E('div', { - 'class': 'tor-preset' + (self.currentPreset === preset.id ? ' active' : ''), - 'data-preset': preset.id, - 'click': L.bind(function() { this.handlePresetSelect(preset.id); }, self) - }, [ - E('div', { 'class': 'tor-preset-icon' }, api.getPresetIcon(preset.icon)), - E('div', { 'class': 'tor-preset-name' }, preset.name), - E('div', { 'class': 'tor-preset-desc' }, preset.description) - ]); - }) - ), + // Presets - Wizard Style + E('div', { 'class': 'tor-presets-wizard', 'style': 'margin-bottom: 24px;' }, [ + E('div', { 'style': 'text-align: center; margin-bottom: 16px;' }, [ + E('span', { 'style': 'font-size: 20px;' }, '\u{1F9D9}\u200D\u2642\uFE0F'), + E('span', { 'style': 'font-weight: 600; margin-left: 8px;' }, _('Choose Your Protection Level')) + ]), + E('div', { 'class': 'tor-presets', 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px;' }, + [ + { id: 'anonymous', name: _('Full Anonymity'), icon: '\u{1F6E1}', emoji: '\u{1F9D9}', desc: _('All traffic through Tor'), features: ['\u{2705} ' + _('Kill Switch'), '\u{2705} ' + _('DNS Protection'), '\u{2705} ' + _('Full Routing')] }, + { id: 'selective', name: _('Selective Apps'), icon: '\u{1F3AF}', emoji: '\u{1F50D}', desc: _('SOCKS proxy mode'), features: ['\u{26AA} ' + _('No Kill Switch'), '\u{26AA} ' + _('Manual Config'), '\u{2705} ' + _('App Control')] }, + { id: 'censored', name: _('Bypass Censorship'), icon: '\u{1F513}', emoji: '\u{1F30D}', desc: _('Bridge connections'), features: ['\u{2705} ' + _('obfs4 Bridges'), '\u{2705} ' + _('Anti-Censorship'), '\u{2705} ' + _('Stealth Mode')] } + ].map(function(preset) { + var isSelected = self.currentPreset === preset.id; + return E('div', { + 'class': 'tor-preset' + (isSelected ? ' active' : ''), + 'data-preset': preset.id, + 'style': 'background: var(--tor-bg-card, #1a1a24); border-radius: 12px; padding: 16px; cursor: pointer; border: 2px solid ' + (isSelected ? '#7c3aed' : 'transparent') + '; transition: all 0.2s;', + 'click': L.bind(function() { this.handlePresetSelect(preset.id); }, self) + }, [ + E('div', { 'style': 'text-align: center; margin-bottom: 8px;' }, [ + E('span', { 'style': 'font-size: 32px;' }, preset.emoji), + isSelected ? E('span', { 'style': 'position: absolute; margin-left: -8px; font-size: 14px;' }, '\u{2714}\uFE0F') : '' + ]), + E('div', { 'style': 'font-weight: 600; text-align: center; margin-bottom: 4px;' }, preset.name), + E('div', { 'style': 'font-size: 11px; color: var(--tor-text-muted, #a0a0b0); text-align: center; margin-bottom: 8px;' }, preset.desc), + E('div', { 'style': 'font-size: 10px; color: var(--tor-text-muted, #a0a0b0);' }, + preset.features.map(function(f) { + return E('div', { 'style': 'margin: 2px 0;' }, f); + }) + ) + ]); + }) + ) + ]), // Quick Stats E('div', { 'class': 'tor-quick-stats' }, [ @@ -442,42 +491,109 @@ return view.extend({ ]) ]), - // Actions Card + // Actions Card - Enhanced Wizard Style E('div', { 'class': 'tor-card' }, [ E('div', { 'class': 'tor-card-header' }, [ E('div', { 'class': 'tor-card-title' }, [ - E('span', { 'class': 'tor-card-title-icon' }, '\u26A1'), + E('span', { 'class': 'tor-card-title-icon' }, '\u{26A1}'), _('Quick Actions') + ]), + E('span', { 'style': 'font-size: 12px; color: var(--tor-text-muted);' }, '\u{1F9D9} ' + _('Wizard Tools')) + ]), + E('div', { 'class': 'tor-card-body' }, [ + E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px;' }, [ + E('button', { + 'class': 'tor-btn tor-btn-primary', + 'style': 'display: flex; flex-direction: column; align-items: center; padding: 16px; border-radius: 12px;', + 'click': L.bind(this.handleNewIdentity, this), + 'disabled': !isActive + }, [ + E('span', { 'style': 'font-size: 24px; margin-bottom: 4px;' }, '\u{1F504}'), + E('span', {}, _('New Identity')) + ]), + E('button', { + 'class': 'tor-btn', + 'style': 'display: flex; flex-direction: column; align-items: center; padding: 16px; border-radius: 12px;', + 'click': L.bind(this.handleLeakTest, this), + 'disabled': !isActive + }, [ + E('span', { 'style': 'font-size: 24px; margin-bottom: 4px;' }, '\u{1F50D}'), + E('span', {}, _('Leak Test')) + ]), + E('button', { + 'class': 'tor-btn tor-btn-warning', + 'style': 'display: flex; flex-direction: column; align-items: center; padding: 16px; border-radius: 12px;', + 'click': L.bind(this.handleRestart, this) + }, [ + E('span', { 'style': 'font-size: 24px; margin-bottom: 4px;' }, '\u{1F504}'), + E('span', {}, _('Restart')) + ]), + E('a', { + 'class': 'tor-btn', + 'style': 'display: flex; flex-direction: column; align-items: center; padding: 16px; border-radius: 12px; text-decoration: none;', + 'href': L.url('admin', 'services', 'tor-shield', 'circuits') + }, [ + E('span', { 'style': 'font-size: 24px; margin-bottom: 4px;' }, '\u{1F5FA}'), + E('span', {}, _('Circuits')) + ]), + E('a', { + 'class': 'tor-btn', + 'style': 'display: flex; flex-direction: column; align-items: center; padding: 16px; border-radius: 12px; text-decoration: none;', + 'href': L.url('admin', 'services', 'tor-shield', 'hidden-services') + }, [ + E('span', { 'style': 'font-size: 24px; margin-bottom: 4px;' }, '\u{1F9C5}'), + E('span', {}, _('.onion Sites')) + ]), + E('a', { + 'class': 'tor-btn', + 'style': 'display: flex; flex-direction: column; align-items: center; padding: 16px; border-radius: 12px; text-decoration: none;', + 'href': L.url('admin', 'services', 'tor-shield', 'bridges') + }, [ + E('span', { 'style': 'font-size: 24px; margin-bottom: 4px;' }, '\u{1F309}'), + E('span', {}, _('Bridges')) + ]), + E('a', { + 'class': 'tor-btn', + 'style': 'display: flex; flex-direction: column; align-items: center; padding: 16px; border-radius: 12px; text-decoration: none;', + 'href': L.url('admin', 'services', 'tor-shield', 'settings') + }, [ + E('span', { 'style': 'font-size: 24px; margin-bottom: 4px;' }, '\u{2699}\uFE0F'), + E('span', {}, _('Settings')) + ]) + ]) + ]) + ]), + + // Features Guide Card + E('div', { 'class': 'tor-card', 'style': 'background: linear-gradient(135deg, rgba(124, 58, 237, 0.1) 0%, rgba(6, 182, 212, 0.1) 100%);' }, [ + E('div', { 'class': 'tor-card-header' }, [ + E('div', { 'class': 'tor-card-title' }, [ + E('span', { 'class': 'tor-card-title-icon' }, '\u{1F4D6}'), + _('How Tor Shield Works') ]) ]), E('div', { 'class': 'tor-card-body' }, [ - E('div', { 'style': 'display: flex; gap: 12px; flex-wrap: wrap;' }, [ - E('button', { - 'class': 'tor-btn tor-btn-primary', - 'click': L.bind(this.handleNewIdentity, this), - 'disabled': !isActive - }, ['\uD83D\uDD04 ', _('New Identity')]), - E('button', { - 'class': 'tor-btn', - 'click': L.bind(this.handleLeakTest, this), - 'disabled': !isActive - }, ['\uD83D\uDD0D ', _('Leak Test')]), - E('button', { - 'class': 'tor-btn tor-btn-warning', - 'click': L.bind(this.handleRestart, this) - }, ['\u21BB ', _('Restart')]), - E('a', { - 'class': 'tor-btn', - 'href': L.url('admin', 'services', 'tor-shield', 'circuits') - }, ['\uD83D\uDDFA ', _('View Circuits')]), - E('a', { - 'class': 'tor-btn', - 'href': L.url('admin', 'services', 'tor-shield', 'hidden-services') - }, ['\uD83E\uDDC5 ', _('Hidden Services')]), - E('a', { - 'class': 'tor-btn', - 'href': L.url('admin', 'services', 'tor-shield', 'settings') - }, ['\u2699 ', _('Settings')]) + E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;' }, [ + E('div', { 'style': 'text-align: center; padding: 12px;' }, [ + E('div', { 'style': 'font-size: 32px; margin-bottom: 8px;' }, '\u{1F512}'), + E('div', { 'style': 'font-weight: 600; margin-bottom: 4px;' }, _('Encrypted')), + E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted);' }, _('3 layers of encryption protect your data')) + ]), + E('div', { 'style': 'text-align: center; padding: 12px;' }, [ + E('div', { 'style': 'font-size: 32px; margin-bottom: 8px;' }, '\u{1F465}'), + E('div', { 'style': 'font-weight: 600; margin-bottom: 4px;' }, _('Anonymous')), + E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted);' }, _('Your real IP is hidden from websites')) + ]), + E('div', { 'style': 'text-align: center; padding: 12px;' }, [ + E('div', { 'style': 'font-size: 32px; margin-bottom: 8px;' }, '\u{1F310}'), + E('div', { 'style': 'font-weight: 600; margin-bottom: 4px;' }, _('Decentralized')), + E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted);' }, _('Traffic routed through volunteer relays')) + ]), + E('div', { 'style': 'text-align: center; padding: 12px;' }, [ + E('div', { 'style': 'font-size: 32px; margin-bottom: 8px;' }, '\u{1F6E1}'), + E('div', { 'style': 'font-weight: 600; margin-bottom: 4px;' }, _('Kill Switch')), + E('div', { 'style': 'font-size: 12px; color: var(--tor-text-muted);' }, _('Blocks traffic if Tor disconnects')) + ]) ]) ]) ]), diff --git a/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl index bea04b58..03e8702b 100644 --- a/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl +++ b/package/secubox/secubox-app-haproxy/files/usr/sbin/haproxyctl @@ -484,7 +484,7 @@ _generate_frontends() { # HTTP Frontend cat << EOF frontend http-in - bind *:$http_port + bind *:$http_port,[::]:$http_port mode http # ACME challenge routing (no HAProxy restart needed for cert issuance) @@ -509,7 +509,7 @@ EOF if [ -d "$CERTS_PATH" ] && ls "$CERTS_PATH"/*.pem >/dev/null 2>&1; then cat << EOF frontend https-in - bind *:$https_port ssl crt $CONTAINER_CERTS_PATH/ alpn h2,http/1.1 + bind *:$https_port,[::]:$https_port ssl crt $CONTAINER_CERTS_PATH/ alpn h2,http/1.1 mode http http-request set-header X-Forwarded-Proto https http-request set-header X-Real-IP %[src] @@ -537,7 +537,7 @@ _add_ssl_redirect() { local acl_name=$(echo "$domain" | tr '.' '_' | tr '-' '_') echo " acl host_${acl_name} hdr(host) -i $domain" - echo " http-request redirect scheme https code 301 if host_${acl_name} !{ ssl_fc }" + echo " http-request redirect scheme https code 301 if host_${acl_name} !{ ssl_fc } !is_acme_challenge" } _add_vhost_acl() { diff --git a/secubox-tools/local-build.sh b/secubox-tools/local-build.sh index 9b6cb4e4..4ceeb386 100755 --- a/secubox-tools/local-build.sh +++ b/secubox-tools/local-build.sh @@ -625,6 +625,85 @@ sync_packages_to_local_feed() { print_success "Synchronized $pkg_count packages to local-feed" } +# Deploy packages to router +deploy_packages() { + local router="$1" + local packages="$2" + local ssh_opts="-o StrictHostKeyChecking=no -o ConnectTimeout=10" + + # Test connectivity + print_info "Testing connection to router..." + if ! ssh $ssh_opts root@$router "echo 'Connected'" 2>/dev/null; then + print_error "Cannot connect to router at $router" + return 1 + fi + + # Find packages to deploy + local pkg_dir="$SDK_DIR/bin/packages/$ARCH_NAME/secubox" + local target_pkg_dir="$SDK_DIR/bin/targets/$SDK_PATH/packages" + + if [[ -n "$packages" ]]; then + # Deploy specific packages + print_info "Deploying specific packages: $packages" + for pkg in $packages; do + local ipk=$(find "$pkg_dir" "$target_pkg_dir" -name "${pkg}*.ipk" 2>/dev/null | head -1) + if [[ -n "$ipk" ]]; then + print_info "Deploying $(basename "$ipk")..." + scp $ssh_opts "$ipk" root@$router:/tmp/ + ssh $ssh_opts root@$router "opkg install /tmp/$(basename "$ipk") --force-reinstall 2>&1" + else + print_warning "Package not found: $pkg" + fi + done + else + # Deploy all recently built packages + print_info "Deploying all packages from SDK..." + + # Find all IPK files built today + local today=$(date +%Y%m%d) + local ipks=$(find "$pkg_dir" -name "*.ipk" -mtime 0 2>/dev/null) + + if [[ -z "$ipks" ]]; then + print_warning "No recently built packages found" + print_info "Run 'local-build.sh build ' first" + return 1 + fi + + # Copy packages + print_info "Copying packages to router..." + for ipk in $ipks; do + scp $ssh_opts "$ipk" root@$router:/tmp/ + done + + # Install packages + print_info "Installing packages..." + ssh $ssh_opts root@$router "opkg install /tmp/*.ipk --force-reinstall 2>&1" || true + fi + + # Sync feed to router + print_info "Syncing package feed to router..." + local feed_pkg="$SDK_DIR/bin/packages/$ARCH_NAME/secubox" + if [[ -d "$feed_pkg" ]]; then + ssh $ssh_opts root@$router "mkdir -p /www/secubox-feed" + scp $ssh_opts "$feed_pkg"/*.ipk root@$router:/www/secubox-feed/ 2>/dev/null || true + + # Generate Packages index + ssh $ssh_opts root@$router "cd /www/secubox-feed && \ + rm -f Packages Packages.gz && \ + for ipk in *.ipk; do \ + [ -f \"\$ipk\" ] && tar -xzf \"\$ipk\" ./control.tar.gz && \ + tar -xzf control.tar.gz ./control && \ + cat control >> Packages && echo '' >> Packages && \ + rm -f control control.tar.gz; \ + done && \ + gzip -k Packages 2>/dev/null || true" + + print_success "Feed synced to /www/secubox-feed" + fi + + print_success "Deployment complete" +} + # Copy packages to SDK feed copy_packages() { local single_package="$1" @@ -2249,6 +2328,7 @@ COMMANDS: clean Clean build directories clean-all Clean all build directories including OpenWrt source and local-feed sync Sync packages from package/secubox to local-feed + deploy [router] [packages] Deploy packages to router (default: 192.168.255.1) help Show this help message PACKAGES: @@ -2429,6 +2509,13 @@ main() { print_success "Packages synchronized to local-feed" ;; + deploy) + local router="${1:-192.168.255.1}" + local packages="$2" + print_header "Deploying Packages to Router ($router)" + deploy_packages "$router" "$packages" + ;; + help|--help|-h) show_usage ;;